hyperagents 1.2.3rc5.post3__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 (361) hide show
  1. agents/__init__.py +31 -0
  2. agents/background_monitor.py +453 -0
  3. agents/learning.py +460 -0
  4. agents/planner.py +421 -0
  5. agents/transcriber.py +341 -0
  6. civilization/__init__.py +51 -0
  7. civilization/api/__init__.py +7 -0
  8. civilization/api/civilization_api.py +361 -0
  9. civilization/graph/__init__.py +19 -0
  10. civilization/graph/knowledge_linker.py +348 -0
  11. civilization/graph/org_graph.py +348 -0
  12. civilization/graph/process_graph.py +386 -0
  13. civilization/ingestion/__init__.py +20 -0
  14. civilization/ingestion/chunker.py +273 -0
  15. civilization/ingestion/document_ingestor.py +358 -0
  16. civilization/ingestion/embedder.py +262 -0
  17. civilization/ingestion/extractor.py +224 -0
  18. civilization/interview/__init__.py +19 -0
  19. civilization/interview/gap_detector.py +380 -0
  20. civilization/interview/interviewer.py +323 -0
  21. civilization/interview/questioner.py +363 -0
  22. civilization/retrieval/__init__.py +17 -0
  23. civilization/retrieval/civilization_rag.py +316 -0
  24. civilization/retrieval/context_injector.py +342 -0
  25. civilization/retrieval/relevance_ranker.py +287 -0
  26. civilization/schema.py +299 -0
  27. civilization/store.py +177 -0
  28. civilization/sync/__init__.py +16 -0
  29. civilization/sync/confluence_sync.py +317 -0
  30. civilization/sync/gdrive_sync.py +262 -0
  31. civilization/sync/notion_sync.py +237 -0
  32. civilization/versioning/__init__.py +19 -0
  33. civilization/versioning/diff_engine.py +351 -0
  34. civilization/versioning/staleness_detector.py +303 -0
  35. civilization/versioning/version_manager.py +293 -0
  36. cli/__init__.py +1 -0
  37. cli/doctor.py +334 -0
  38. cli/hyperclaw.py +1357 -0
  39. cli/onboarding.py +469 -0
  40. core/__init__.py +1 -0
  41. core/connectors/__init__.py +119 -0
  42. core/connectors/infrastructure.py +772 -0
  43. core/connectors/messaging.py +586 -0
  44. core/connectors/productivity.py +764 -0
  45. core/connectors/smarthome.py +528 -0
  46. core/connectors/social.py +730 -0
  47. core/hyperrouter/__init__.py +23 -0
  48. core/hyperrouter/bandit.py +218 -0
  49. core/hyperrouter/fast_loop.py +68 -0
  50. core/hyperrouter/slow_loop.py +143 -0
  51. core/hyperstate/__init__.py +24 -0
  52. core/hyperstate/certifier.py +154 -0
  53. core/hyperstate/schema.py +160 -0
  54. core/hyperstate/state_manager.py +103 -0
  55. core/hyperstate/store.py +163 -0
  56. core/providers/__init__.py +121 -0
  57. core/providers/embeddings.py +389 -0
  58. core/providers/image.py +534 -0
  59. core/providers/llm.py +1935 -0
  60. core/providers/search.py +608 -0
  61. core/providers/voice.py +474 -0
  62. hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
  63. hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
  64. hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
  65. hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
  66. hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
  67. hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
  68. hyperclaw/__init__.py +67 -0
  69. hyperclaw/__main__.py +220 -0
  70. hyperclaw/agent.py +775 -0
  71. hyperclaw/agent_coordinator.py +577 -0
  72. hyperclaw/anthropic_params.py +225 -0
  73. hyperclaw/api_utils.py +85 -0
  74. hyperclaw/approval_telegram.py +209 -0
  75. hyperclaw/approval_terminal.py +80 -0
  76. hyperclaw/attachments.py +177 -0
  77. hyperclaw/branding.py +55 -0
  78. hyperclaw/brief/__init__.py +11 -0
  79. hyperclaw/brief/cli.py +204 -0
  80. hyperclaw/brief/collect.py +146 -0
  81. hyperclaw/brief/compose.py +333 -0
  82. hyperclaw/brief/diff.py +110 -0
  83. hyperclaw/brief/errors.py +99 -0
  84. hyperclaw/brief/google.py +269 -0
  85. hyperclaw/brief/google_auth.py +294 -0
  86. hyperclaw/brief/links.py +124 -0
  87. hyperclaw/brief/service.py +424 -0
  88. hyperclaw/brief/store.py +205 -0
  89. hyperclaw/brief/tasks.py +200 -0
  90. hyperclaw/chat_scope.py +115 -0
  91. hyperclaw/cli_tui.py +497 -0
  92. hyperclaw/client_guard.py +717 -0
  93. hyperclaw/connection_check.py +113 -0
  94. hyperclaw/cost_guard.py +1728 -0
  95. hyperclaw/default_config/agents.yaml +534 -0
  96. hyperclaw/default_config/hyperclaw.yaml +27 -0
  97. hyperclaw/default_config/models.yaml +161 -0
  98. hyperclaw/deployment.py +39 -0
  99. hyperclaw/digest/__init__.py +9 -0
  100. hyperclaw/digest/cli.py +117 -0
  101. hyperclaw/digest/errors.py +100 -0
  102. hyperclaw/digest/gitlab.py +187 -0
  103. hyperclaw/digest/http.py +113 -0
  104. hyperclaw/digest/redact.py +32 -0
  105. hyperclaw/digest/service.py +271 -0
  106. hyperclaw/digest/slack.py +223 -0
  107. hyperclaw/digest/state.py +124 -0
  108. hyperclaw/digest/summarize.py +198 -0
  109. hyperclaw/digest/tokens.py +143 -0
  110. hyperclaw/distribution.py +46 -0
  111. hyperclaw/documents.py +351 -0
  112. hyperclaw/feeds.py +339 -0
  113. hyperclaw/google_workspace/__init__.py +5 -0
  114. hyperclaw/google_workspace/api.py +151 -0
  115. hyperclaw/google_workspace/auth.py +397 -0
  116. hyperclaw/google_workspace/calendar.py +157 -0
  117. hyperclaw/google_workspace/cli.py +72 -0
  118. hyperclaw/google_workspace/drive.py +236 -0
  119. hyperclaw/google_workspace/errors.py +58 -0
  120. hyperclaw/google_workspace/gmail.py +295 -0
  121. hyperclaw/google_workspace/meet.py +122 -0
  122. hyperclaw/google_workspace/tools.py +319 -0
  123. hyperclaw/hyperspeed.py +57 -0
  124. hyperclaw/integrations_layer.py +1427 -0
  125. hyperclaw/live_state.py +188 -0
  126. hyperclaw/managed_config.py +245 -0
  127. hyperclaw/media_hub.py +179 -0
  128. hyperclaw/memory_manager.py +584 -0
  129. hyperclaw/model_catalog.py +551 -0
  130. hyperclaw/model_policy.py +211 -0
  131. hyperclaw/model_router.py +595 -0
  132. hyperclaw/model_selector.py +72 -0
  133. hyperclaw/naming.py +284 -0
  134. hyperclaw/notices.py +65 -0
  135. hyperclaw/oai_stream.py +106 -0
  136. hyperclaw/orchestrator.py +591 -0
  137. hyperclaw/outbox.py +82 -0
  138. hyperclaw/persona.py +367 -0
  139. hyperclaw/prometheus.py +219 -0
  140. hyperclaw/prompt_assembly.py +157 -0
  141. hyperclaw/provider_setup.py +146 -0
  142. hyperclaw/providers.py +340 -0
  143. hyperclaw/qualification.py +341 -0
  144. hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
  145. hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
  146. hyperclaw/reasoning_display.py +35 -0
  147. hyperclaw/scheduler.py +454 -0
  148. hyperclaw/server.py +847 -0
  149. hyperclaw/setup.py +627 -0
  150. hyperclaw/skills.py +717 -0
  151. hyperclaw/solomon.py +442 -0
  152. hyperclaw/swarm.py +533 -0
  153. hyperclaw/taint.py +832 -0
  154. hyperclaw/team/__init__.py +28 -0
  155. hyperclaw/team/dispatcher.py +431 -0
  156. hyperclaw/team/genesis.py +106 -0
  157. hyperclaw/team/lead.py +110 -0
  158. hyperclaw/team/model.py +135 -0
  159. hyperclaw/team/morning.py +238 -0
  160. hyperclaw/team/repos.py +66 -0
  161. hyperclaw/team/roster.py +264 -0
  162. hyperclaw/team/specialist.py +235 -0
  163. hyperclaw/team/status.py +163 -0
  164. hyperclaw/team/watchers.py +112 -0
  165. hyperclaw/teamsetup/__init__.py +17 -0
  166. hyperclaw/teamsetup/checks.py +208 -0
  167. hyperclaw/teamsetup/item.py +391 -0
  168. hyperclaw/teamsetup/oauth_loopback.py +84 -0
  169. hyperclaw/teamsetup/onboard.py +550 -0
  170. hyperclaw/teamsetup/profile.py +82 -0
  171. hyperclaw/teamsetup/secrets.py +313 -0
  172. hyperclaw/teamsetup/selfcheck.py +295 -0
  173. hyperclaw/teamsetup/service.py +495 -0
  174. hyperclaw/teamsetup/slack_oauth.py +442 -0
  175. hyperclaw/teamsetup/tty.py +62 -0
  176. hyperclaw/telegram_bot.py +1106 -0
  177. hyperclaw/telegram_intake.py +126 -0
  178. hyperclaw/telegram_pairing.py +360 -0
  179. hyperclaw/terminal_provider.py +323 -0
  180. hyperclaw/tools/__init__.py +1 -0
  181. hyperclaw/tools/approval.py +949 -0
  182. hyperclaw/tools/args.py +212 -0
  183. hyperclaw/tools/catalog.py +244 -0
  184. hyperclaw/tools/classify.py +266 -0
  185. hyperclaw/tools/effects.py +724 -0
  186. hyperclaw/tools/egress.py +210 -0
  187. hyperclaw/tools/envelope.py +188 -0
  188. hyperclaw/tools/guard.py +367 -0
  189. hyperclaw/tools/ledger.py +339 -0
  190. hyperclaw/tools/receipts.py +70 -0
  191. hyperclaw/tools/registry.py +318 -0
  192. hyperclaw/tools/service_guard.py +151 -0
  193. hyperclaw/tools/surfaces.py +54 -0
  194. hyperclaw/tools/workspace.py +87 -0
  195. hyperclaw/trading/__init__.py +77 -0
  196. hyperclaw/truth_guard.py +791 -0
  197. hyperclaw/tts.py +124 -0
  198. hyperclaw/tui.py +4255 -0
  199. hyperclaw/tui_bridge.py +938 -0
  200. hyperclaw/unattended.py +60 -0
  201. hyperclaw/updates/__init__.py +54 -0
  202. hyperclaw/updates/check.py +131 -0
  203. hyperclaw/updates/index.py +178 -0
  204. hyperclaw/updates/install.py +110 -0
  205. hyperclaw/updates/managed.py +161 -0
  206. hyperclaw/updates/manifest.py +138 -0
  207. hyperclaw/updates/notices.py +86 -0
  208. hyperclaw/updates/paths.py +53 -0
  209. hyperclaw/updates/release.py +146 -0
  210. hyperclaw/updates/upgrade.py +253 -0
  211. hyperclaw/updates/versioning.py +104 -0
  212. hyperclaw/userconfig.py +119 -0
  213. hyperclaw/xai.py +83 -0
  214. integrations/__init__.py +31 -0
  215. integrations/automation/__init__.py +1 -0
  216. integrations/automation/make.py +87 -0
  217. integrations/automation/zapier.py +87 -0
  218. integrations/base.py +476 -0
  219. integrations/communication/__init__.py +1 -0
  220. integrations/communication/twilio.py +279 -0
  221. integrations/communication/zoom.py +290 -0
  222. integrations/data/__init__.py +1 -0
  223. integrations/data/google_analytics.py +135 -0
  224. integrations/data/postgres.py +182 -0
  225. integrations/data/supabase_connector.py +202 -0
  226. integrations/data/weather_feeds.py +450 -0
  227. integrations/developer/__init__.py +9 -0
  228. integrations/developer/github.py +406 -0
  229. integrations/developer/gitlab.py +298 -0
  230. integrations/enterprise/__init__.py +13 -0
  231. integrations/enterprise/confluence.py +266 -0
  232. integrations/enterprise/hubspot.py +293 -0
  233. integrations/enterprise/jira.py +314 -0
  234. integrations/enterprise/salesforce.py +286 -0
  235. integrations/finance/__init__.py +1 -0
  236. integrations/finance/polymarket_client.py +247 -0
  237. integrations/finance/quickbooks.py +281 -0
  238. integrations/finance/stripe.py +261 -0
  239. integrations/gateway.py +238 -0
  240. integrations/messaging/__init__.py +32 -0
  241. integrations/messaging/discord.py +159 -0
  242. integrations/messaging/email.py +401 -0
  243. integrations/messaging/imessage.py +443 -0
  244. integrations/messaging/signal.py +175 -0
  245. integrations/messaging/slack.py +217 -0
  246. integrations/messaging/sms.py +175 -0
  247. integrations/messaging/teams.py +208 -0
  248. integrations/messaging/telegram.py +260 -0
  249. integrations/messaging/whatsapp.py +203 -0
  250. integrations/productivity/__init__.py +17 -0
  251. integrations/productivity/airtable.py +217 -0
  252. integrations/productivity/asana.py +248 -0
  253. integrations/productivity/google/__init__.py +19 -0
  254. integrations/productivity/google/base.py +89 -0
  255. integrations/productivity/google/calendar.py +272 -0
  256. integrations/productivity/google/docs.py +202 -0
  257. integrations/productivity/google/drive.py +268 -0
  258. integrations/productivity/google/gmail.py +279 -0
  259. integrations/productivity/google/meet.py +218 -0
  260. integrations/productivity/google/sheets.py +222 -0
  261. integrations/productivity/google/tasks.py +199 -0
  262. integrations/productivity/linear.py +279 -0
  263. integrations/productivity/microsoft/__init__.py +13 -0
  264. integrations/productivity/microsoft/base.py +93 -0
  265. integrations/productivity/microsoft/calendar.py +207 -0
  266. integrations/productivity/microsoft/onedrive.py +222 -0
  267. integrations/productivity/microsoft/outlook.py +241 -0
  268. integrations/productivity/microsoft/sharepoint.py +211 -0
  269. integrations/productivity/notion.py +243 -0
  270. integrations/productivity/todoist.py +250 -0
  271. integrations/productivity/trello.py +244 -0
  272. integrations/storage/__init__.py +1 -0
  273. integrations/storage/box.py +279 -0
  274. integrations/storage/dropbox.py +215 -0
  275. integrations/telegram_inbound.py +179 -0
  276. integrations/telegram_poller.py +151 -0
  277. memory/__init__.py +1 -0
  278. memory/causal_graph.py +125 -0
  279. memory/impact_tracker.py +88 -0
  280. memory/vector_memory.py +436 -0
  281. models/__init__.py +12 -0
  282. models/chatjimmy_client.py +210 -0
  283. models/claude_client.py +235 -0
  284. models/claude_code_subagent.py +192 -0
  285. models/openai_client.py +120 -0
  286. models/router.py +162 -0
  287. recursive/discovery_loop.py +118 -0
  288. recursive/optimization_loop.py +135 -0
  289. recursive/validation_loop.py +38 -0
  290. security/__init__.py +1 -0
  291. security/audit_logger.py +106 -0
  292. security/filesystem_guard.py +100 -0
  293. security/hypershield.py +99 -0
  294. security/network_guard.py +97 -0
  295. security/policy_engine.py +198 -0
  296. swarm/__init__.py +1 -0
  297. swarm/agents/__init__.py +0 -0
  298. swarm/agents/base.py +230 -0
  299. swarm/agents/business/__init__.py +0 -0
  300. swarm/agents/business/counsel.py +31 -0
  301. swarm/agents/business/herald.py +32 -0
  302. swarm/agents/business/ledger.py +26 -0
  303. swarm/agents/business/nexus.py +30 -0
  304. swarm/agents/business/ops.py +30 -0
  305. swarm/agents/business/pipeline.py +28 -0
  306. swarm/agents/business/revenue.py +30 -0
  307. swarm/agents/business/sovereign.py +31 -0
  308. swarm/agents/business/strategos.py +27 -0
  309. swarm/agents/business/talent.py +27 -0
  310. swarm/agents/business/venture.py +32 -0
  311. swarm/agents/comms/__init__.py +1 -0
  312. swarm/agents/comms/cipher.py +32 -0
  313. swarm/agents/comms/echo_comms.py +31 -0
  314. swarm/agents/comms/envoy.py +31 -0
  315. swarm/agents/comms/herald_scheduler.py +33 -0
  316. swarm/agents/comms/pulse.py +29 -0
  317. swarm/agents/creative/__init__.py +0 -0
  318. swarm/agents/creative/author.py +27 -0
  319. swarm/agents/creative/lens.py +25 -0
  320. swarm/agents/executive/__init__.py +1 -0
  321. swarm/agents/intelligence/__init__.py +1 -0
  322. swarm/agents/intelligence/arbiter.py +30 -0
  323. swarm/agents/intelligence/sentinel.py +30 -0
  324. swarm/agents/media/__init__.py +0 -0
  325. swarm/agents/personal/__init__.py +0 -0
  326. swarm/agents/personal/atlas.py +27 -0
  327. swarm/agents/personal/hearth.py +26 -0
  328. swarm/agents/personal/midas.py +27 -0
  329. swarm/agents/personal/navigator.py +27 -0
  330. swarm/agents/personal/nourish.py +31 -0
  331. swarm/agents/personal/vitals.py +33 -0
  332. swarm/agents/recursive/__init__.py +0 -0
  333. swarm/agents/recursive/alchemist.py +73 -0
  334. swarm/agents/recursive/calibrator.py +45 -0
  335. swarm/agents/recursive/scout.py +97 -0
  336. swarm/agents/scientific/__init__.py +0 -0
  337. swarm/agents/scientific/cosmos.py +36 -0
  338. swarm/agents/scientific/gaia.py +26 -0
  339. swarm/agents/scientific/medicus.py +33 -0
  340. swarm/agents/scientific/oracle_agent.py +39 -0
  341. swarm/agents/scientific/scribe.py +39 -0
  342. swarm/agents/talent/__init__.py +0 -0
  343. swarm/agents/talent/deal.py +28 -0
  344. swarm/agents/talent/roster.py +29 -0
  345. swarm/agents/talent/scout.py +29 -0
  346. swarm/agents/talent/stage.py +27 -0
  347. swarm/agents/tech/__init__.py +0 -0
  348. swarm/agents/tech/aegis.py +28 -0
  349. swarm/agents/tech/bridge.py +28 -0
  350. swarm/agents/tech/forge.py +28 -0
  351. swarm/agents/trading/__init__.py +1 -0
  352. swarm/agents/trading/global_prediction_engine.py +611 -0
  353. swarm/agents/trading/polymarket_trader.py +165 -0
  354. swarm/agents/trading/prediction_strategist.py +218 -0
  355. swarm/autogen_bridge.py +68 -0
  356. swarm/bid_protocol.py +196 -0
  357. swarm/dispatcher.py +527 -0
  358. swarm/nexus.py +181 -0
  359. swarm/registry.py +112 -0
  360. telegram_listener.py +403 -0
  361. ui/tui/app.py +214 -0
@@ -0,0 +1,584 @@
1
+ """
2
+ HyperClaw Memory Manager
3
+ Handles memory persistence across sessions with database + file fallback.
4
+ """
5
+
6
+ import asyncio
7
+ import json
8
+ import logging
9
+ import os
10
+ import re
11
+ from datetime import datetime, timedelta
12
+ from pathlib import Path
13
+ from typing import Any, Optional
14
+ from dataclasses import dataclass, field
15
+
16
+ from hyperclaw import taint
17
+ from hyperclaw.taint import training_sink
18
+
19
+ logger = logging.getLogger("hyperclaw.memory")
20
+
21
+ # Paths
22
+ HYPERCLAW_ROOT = Path(os.environ.get("HYPERCLAW_ROOT", Path.home() / ".hyperclaw"))
23
+ MEMORY_PATH = HYPERCLAW_ROOT / "memory"
24
+ WORKSPACE_PATH = HYPERCLAW_ROOT / "workspace"
25
+
26
+
27
+ @dataclass
28
+ class Memory:
29
+ """A single memory entry."""
30
+ id: str
31
+ content: str
32
+ memory_type: str # 'episode', 'semantic', 'instinct', 'dream'
33
+ domain: Optional[str] = None
34
+ importance: float = 0.5
35
+ summary: Optional[str] = None
36
+ source: str = "conversation"
37
+ is_core: bool = False
38
+ metadata: dict = field(default_factory=dict)
39
+ created_at: datetime = field(default_factory=datetime.now)
40
+ embedding: Optional[list] = None
41
+
42
+
43
+ class MemoryManager:
44
+ """
45
+ Manages memory persistence across sessions.
46
+ Uses database when available, falls back to file storage.
47
+ """
48
+
49
+ def __init__(self, db_pool=None):
50
+ self.db_pool = db_pool
51
+ self._file_cache: dict[str, list[Memory]] = {}
52
+ self._conversation_history: dict[str, list[dict]] = {}
53
+ self._embeddings_client = None
54
+
55
+ async def initialize(self):
56
+ """Initialize the memory manager."""
57
+ # Ensure directories exist
58
+ MEMORY_PATH.mkdir(parents=True, exist_ok=True)
59
+ (MEMORY_PATH / "daily").mkdir(exist_ok=True)
60
+
61
+ # Load file-based memories
62
+ await self._load_file_memories()
63
+
64
+ logger.info("Memory manager initialized")
65
+
66
+ async def _load_file_memories(self):
67
+ """Load memories from markdown files."""
68
+ # Load instincts
69
+ instincts_file = MEMORY_PATH / "instincts.md"
70
+ if instincts_file.exists():
71
+ content = instincts_file.read_text(encoding="utf-8")
72
+ self._file_cache["instincts"] = self._parse_markdown_list(content, "instinct")
73
+
74
+ # Load core episodes
75
+ episodes_file = MEMORY_PATH / "core-episodes.md"
76
+ if episodes_file.exists():
77
+ content = episodes_file.read_text(encoding="utf-8")
78
+ self._file_cache["episodes"] = self._parse_markdown_list(content, "episode")
79
+
80
+ # Load working memory
81
+ memory_file = WORKSPACE_PATH / "MEMORY.md"
82
+ if memory_file.exists():
83
+ content = memory_file.read_text(encoding="utf-8")
84
+ self._file_cache["working"] = self._parse_markdown_list(content, "semantic")
85
+
86
+ def _parse_markdown_list(self, content: str, memory_type: str) -> list[Memory]:
87
+ """Parse markdown bullet points into Memory objects."""
88
+ memories = []
89
+ lines = content.split("\n")
90
+
91
+ for i, line in enumerate(lines):
92
+ line = line.strip()
93
+ if line.startswith("- ") or line.startswith("* "):
94
+ text = line[2:].strip()
95
+ if text and not text.startswith("*"): # Skip formatting markers
96
+ memories.append(Memory(
97
+ id=f"file_{memory_type}_{i}",
98
+ content=text,
99
+ memory_type=memory_type,
100
+ source="file",
101
+ is_core=(memory_type in ["instinct", "episode"]),
102
+ ))
103
+
104
+ return memories
105
+
106
+ # =========================================================================
107
+ # CONVERSATION HISTORY
108
+ # =========================================================================
109
+
110
+ def get_conversation_history(self, session_id: str, limit: int = 50) -> list[dict]:
111
+ """Get conversation history for a session."""
112
+ history = self._conversation_history.get(session_id, [])
113
+ return history[-limit:] if len(history) > limit else history
114
+
115
+ def add_message(self, session_id: str, role: str, content: str, metadata: dict = None):
116
+ """Add a message to conversation history."""
117
+ if session_id not in self._conversation_history:
118
+ self._conversation_history[session_id] = []
119
+
120
+ message = {
121
+ "role": role,
122
+ "content": content,
123
+ "timestamp": datetime.now().isoformat(),
124
+ **(metadata or {})
125
+ }
126
+
127
+ self._conversation_history[session_id].append(message)
128
+
129
+ # Trim to max size
130
+ max_history = 100
131
+ if len(self._conversation_history[session_id]) > max_history:
132
+ self._conversation_history[session_id] = self._conversation_history[session_id][-max_history:]
133
+
134
+ async def save_conversation(self, session_id: str):
135
+ """Persist conversation to storage. Messages holding connector content
136
+ carry taint.TAINT_KEY so a reloaded conversation is re-flagged."""
137
+ history = taint.tag_messages(self._conversation_history.get(session_id, []))
138
+ if not history:
139
+ return
140
+
141
+ if self.db_pool:
142
+ await self._save_conversation_db(session_id, history)
143
+ else:
144
+ await self._save_conversation_file(session_id, history)
145
+
146
+ async def _save_conversation_db(self, session_id: str, history: list[dict]):
147
+ """Save conversation to database."""
148
+ try:
149
+ async with self.db_pool.acquire() as conn:
150
+ # Get or create conversation
151
+ conv = await conn.fetchrow(
152
+ """
153
+ INSERT INTO conversations (session_id, channel, last_message_at)
154
+ VALUES ($1, 'api', NOW())
155
+ ON CONFLICT (session_id) DO UPDATE SET last_message_at = NOW()
156
+ RETURNING id
157
+ """,
158
+ session_id
159
+ )
160
+
161
+ # Insert messages
162
+ for msg in history[-10:]: # Only save recent messages
163
+ await conn.execute(
164
+ """
165
+ INSERT INTO messages (conversation_id, role, content, created_at, metadata)
166
+ VALUES ($1, $2, $3, $4, $5)
167
+ """,
168
+ conv["id"],
169
+ msg["role"],
170
+ msg["content"],
171
+ datetime.fromisoformat(msg["timestamp"]),
172
+ json.dumps({taint.TAINT_KEY: msg[taint.TAINT_KEY]}
173
+ if msg.get(taint.TAINT_KEY) else {}),
174
+ )
175
+ except Exception as e:
176
+ logger.error(f"Failed to save conversation to DB: {e}")
177
+
178
+ async def _save_conversation_file(self, session_id: str, history: list[dict]):
179
+ """Save conversation to file."""
180
+ try:
181
+ filepath = MEMORY_PATH / "conversations" / f"{session_id}.json"
182
+ filepath.parent.mkdir(exist_ok=True)
183
+ with open(filepath, 'w') as f:
184
+ json.dump(history, f, indent=2)
185
+ except Exception as e:
186
+ logger.error(f"Failed to save conversation to file: {e}")
187
+
188
+ async def load_conversation(self, session_id: str) -> list[dict]:
189
+ """Load conversation from storage."""
190
+ if self.db_pool:
191
+ return await self._load_conversation_db(session_id)
192
+ else:
193
+ return await self._load_conversation_file(session_id)
194
+
195
+ async def _load_conversation_db(self, session_id: str) -> list[dict]:
196
+ """Load conversation from database."""
197
+ try:
198
+ async with self.db_pool.acquire() as conn:
199
+ rows = await conn.fetch(
200
+ """
201
+ SELECT m.role, m.content, m.created_at, m.metadata
202
+ FROM messages m
203
+ JOIN conversations c ON m.conversation_id = c.id
204
+ WHERE c.session_id = $1
205
+ ORDER BY m.created_at DESC
206
+ LIMIT 50
207
+ """,
208
+ session_id
209
+ )
210
+ history = []
211
+ for r in reversed(rows):
212
+ msg = {"role": r["role"], "content": r["content"],
213
+ "timestamp": r["created_at"].isoformat()}
214
+ meta = r["metadata"]
215
+ if isinstance(meta, str):
216
+ meta = json.loads(meta or "{}")
217
+ if isinstance(meta, dict) and meta.get(taint.TAINT_KEY):
218
+ msg[taint.TAINT_KEY] = meta[taint.TAINT_KEY]
219
+ history.append(msg)
220
+ history = taint.restore_messages(session_id, history)
221
+ self._conversation_history[session_id] = history
222
+ return history
223
+ except Exception as e:
224
+ logger.error(f"Failed to load conversation from DB: {e}")
225
+ return []
226
+
227
+ async def _load_conversation_file(self, session_id: str) -> list[dict]:
228
+ """Load conversation from file."""
229
+ try:
230
+ filepath = MEMORY_PATH / "conversations" / f"{session_id}.json"
231
+ if filepath.exists():
232
+ with open(filepath) as f:
233
+ history = json.load(f)
234
+ history = taint.restore_messages(session_id, history)
235
+ self._conversation_history[session_id] = history
236
+ return history
237
+ except Exception as e:
238
+ logger.error(f"Failed to load conversation from file: {e}")
239
+ return []
240
+
241
+ # =========================================================================
242
+ # MEMORY OPERATIONS
243
+ # =========================================================================
244
+
245
+ @training_sink("memory_manager.remember", own_words=True,
246
+ own_words_unless=lambda a: a.get("source") == "auto_store")
247
+ async def remember(
248
+ self,
249
+ content: str,
250
+ memory_type: str = "episode",
251
+ domain: str = None,
252
+ importance: float = 0.5,
253
+ is_core: bool = False,
254
+ metadata: dict = None,
255
+ source: str = "conversation",
256
+ ) -> str:
257
+ """Store a new memory. A memory the user asks for in their own words
258
+ saves; connector content is refused (taint.TaintedInput says why).
259
+ source="auto_store" (saved without the user asking) is also refused
260
+ in a conversation that read connector content."""
261
+ import uuid
262
+
263
+ memory = Memory(
264
+ id=str(uuid.uuid4()),
265
+ content=content,
266
+ memory_type=memory_type,
267
+ domain=domain,
268
+ importance=importance,
269
+ is_core=is_core,
270
+ metadata=metadata or {},
271
+ source=source,
272
+ )
273
+
274
+ if self.db_pool:
275
+ await self._store_memory_db(memory)
276
+ else:
277
+ await self._store_memory_file(memory)
278
+
279
+ return memory.id
280
+
281
+ @training_sink("memory_manager.store_memory_db", own_words=True)
282
+ async def _store_memory_db(self, memory: Memory):
283
+ """Store memory in database."""
284
+ try:
285
+ # Generate embedding if we have a client
286
+ embedding = None
287
+ if self._embeddings_client and memory.content:
288
+ embedding = await self._get_embedding(memory.content)
289
+
290
+ async with self.db_pool.acquire() as conn:
291
+ await conn.execute(
292
+ """
293
+ INSERT INTO memories (id, memory_type, content, summary, domain,
294
+ importance, embedding, source, is_core, metadata, created_at)
295
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
296
+ """,
297
+ memory.id,
298
+ memory.memory_type,
299
+ memory.content,
300
+ memory.summary,
301
+ memory.domain,
302
+ memory.importance,
303
+ embedding,
304
+ memory.source,
305
+ memory.is_core,
306
+ json.dumps(memory.metadata),
307
+ memory.created_at,
308
+ )
309
+ except Exception as e:
310
+ logger.error(f"Failed to store memory in DB: {e}")
311
+ # Fallback to file
312
+ await self._store_memory_file(memory)
313
+
314
+ async def _store_memory_file(self, memory: Memory):
315
+ """Store memory in file."""
316
+ try:
317
+ # Append to daily log
318
+ today = datetime.now().strftime("%Y-%m-%d")
319
+ daily_file = MEMORY_PATH / "daily" / f"{today}.md"
320
+
321
+ with open(daily_file, "a", encoding="utf-8") as f:
322
+ timestamp = datetime.now().strftime("%H:%M")
323
+ f.write(f"\n- [{timestamp}] [{memory.memory_type}] {memory.content}\n")
324
+
325
+ # If core, also append to core-episodes
326
+ if memory.is_core:
327
+ episodes_file = MEMORY_PATH / "core-episodes.md"
328
+ with open(episodes_file, "a", encoding="utf-8") as f:
329
+ date = datetime.now().strftime("%Y-%m-%d")
330
+ f.write(f"\n- [{date}] {memory.content}\n")
331
+
332
+ except Exception as e:
333
+ logger.error(f"Failed to store memory to file: {e}")
334
+
335
+ async def recall(
336
+ self,
337
+ query: str,
338
+ limit: int = 5,
339
+ memory_type: str = None,
340
+ domain: str = None,
341
+ min_importance: float = 0.0
342
+ ) -> list[Memory]:
343
+ """Recall relevant memories."""
344
+ if self.db_pool:
345
+ return await self._recall_db(query, limit, memory_type, domain, min_importance)
346
+ else:
347
+ return await self._recall_file(query, limit, memory_type, domain)
348
+
349
+ async def _recall_db(
350
+ self,
351
+ query: str,
352
+ limit: int,
353
+ memory_type: str,
354
+ domain: str,
355
+ min_importance: float
356
+ ) -> list[Memory]:
357
+ """Recall from database with semantic search."""
358
+ try:
359
+ # Get embedding for query
360
+ query_embedding = None
361
+ if self._embeddings_client:
362
+ query_embedding = await self._get_embedding(query)
363
+
364
+ async with self.db_pool.acquire() as conn:
365
+ if query_embedding:
366
+ # Semantic search
367
+ rows = await conn.fetch(
368
+ """
369
+ SELECT id, content, summary, memory_type, domain, importance,
370
+ 1 - (embedding <=> $1::vector) as similarity
371
+ FROM memories
372
+ WHERE embedding IS NOT NULL
373
+ AND importance >= $2
374
+ AND ($3::text IS NULL OR memory_type = $3)
375
+ AND ($4::text IS NULL OR domain = $4)
376
+ ORDER BY embedding <=> $1::vector
377
+ LIMIT $5
378
+ """,
379
+ query_embedding,
380
+ min_importance,
381
+ memory_type,
382
+ domain,
383
+ limit
384
+ )
385
+ else:
386
+ # Fallback to text search
387
+ rows = await conn.fetch(
388
+ """
389
+ SELECT id, content, summary, memory_type, domain, importance, 0.5 as similarity
390
+ FROM memories
391
+ WHERE content ILIKE $1
392
+ AND importance >= $2
393
+ AND ($3::text IS NULL OR memory_type = $3)
394
+ AND ($4::text IS NULL OR domain = $4)
395
+ ORDER BY importance DESC, created_at DESC
396
+ LIMIT $5
397
+ """,
398
+ f"%{query}%",
399
+ min_importance,
400
+ memory_type,
401
+ domain,
402
+ limit
403
+ )
404
+
405
+ return [
406
+ Memory(
407
+ id=str(r["id"]),
408
+ content=r["content"],
409
+ memory_type=r["memory_type"],
410
+ domain=r["domain"],
411
+ importance=r["importance"],
412
+ summary=r["summary"],
413
+ metadata={"similarity": r["similarity"]}
414
+ )
415
+ for r in rows
416
+ ]
417
+ except Exception as e:
418
+ logger.error(f"Failed to recall from DB: {e}")
419
+ return await self._recall_file(query, limit, memory_type, domain)
420
+
421
+ async def _recall_file(
422
+ self,
423
+ query: str,
424
+ limit: int,
425
+ memory_type: str,
426
+ domain: str
427
+ ) -> list[Memory]:
428
+ """Recall from file cache with keyword matching."""
429
+ results = []
430
+ query_lower = query.lower()
431
+ query_words = set(query_lower.split())
432
+
433
+ for cache_type, memories in self._file_cache.items():
434
+ for memory in memories:
435
+ if memory_type and memory.memory_type != memory_type:
436
+ continue
437
+ if domain and memory.domain != domain:
438
+ continue
439
+
440
+ # Simple relevance scoring
441
+ content_lower = memory.content.lower()
442
+ matches = sum(1 for word in query_words if word in content_lower)
443
+
444
+ if matches > 0:
445
+ memory.metadata["score"] = matches / len(query_words)
446
+ results.append(memory)
447
+
448
+ # Sort by score and return top results
449
+ results.sort(key=lambda m: m.metadata.get("score", 0), reverse=True)
450
+ return results[:limit]
451
+
452
+ async def _get_embedding(self, text: str) -> list[float]:
453
+ """Get embedding for text."""
454
+ # This would use OpenAI or Anthropic embeddings API
455
+ # For now, return None to use text-based search
456
+ return None
457
+
458
+ # =========================================================================
459
+ # CONTEXT LOADING
460
+ # =========================================================================
461
+
462
+ def get_system_context(self) -> str:
463
+ """Get full system context from workspace files."""
464
+ context_parts = []
465
+
466
+ # Load workspace files
467
+ files_to_load = [
468
+ ("SOUL.md", WORKSPACE_PATH / "SOUL.md"),
469
+ ("IDENTITY.md", WORKSPACE_PATH / "IDENTITY.md"),
470
+ ("USER.md", WORKSPACE_PATH / "USER.md"),
471
+ ]
472
+
473
+ for name, path in files_to_load:
474
+ if path.exists():
475
+ try:
476
+ content = path.read_text(encoding="utf-8")
477
+ context_parts.append(f"## {name}\n{content}\n")
478
+ except Exception as e:
479
+ logger.warning(f"Failed to load {name}: {e}")
480
+
481
+ # Add instincts
482
+ instincts = self._file_cache.get("instincts", [])
483
+ if instincts:
484
+ context_parts.append("## Behavioral Instincts")
485
+ for inst in instincts[:20]:
486
+ context_parts.append(f"- {inst.content}")
487
+ context_parts.append("")
488
+
489
+ # Add recent core episodes
490
+ episodes = self._file_cache.get("episodes", [])
491
+ if episodes:
492
+ context_parts.append("## Key Facts (Core Memory)")
493
+ for ep in episodes[-10:]:
494
+ context_parts.append(f"- {ep.content}")
495
+ context_parts.append("")
496
+
497
+ # Add current date
498
+ context_parts.append(f"\nCurrent date/time: {datetime.now().strftime('%Y-%m-%d %H:%M')}\n")
499
+
500
+ return "\n".join(context_parts)
501
+
502
+ def get_working_memory(self) -> str:
503
+ """Get current working memory content."""
504
+ memory_file = WORKSPACE_PATH / "MEMORY.md"
505
+ if memory_file.exists():
506
+ return memory_file.read_text(encoding="utf-8")
507
+ return ""
508
+
509
+ async def update_working_memory(self, section: str, content: str):
510
+ """Update a section in working memory."""
511
+ memory_file = WORKSPACE_PATH / "MEMORY.md"
512
+
513
+ try:
514
+ if memory_file.exists():
515
+ current = memory_file.read_text(encoding="utf-8")
516
+ else:
517
+ current = "# MEMORY.md - Working Memory\n"
518
+
519
+ # Update or append section
520
+ section_pattern = rf"(## {re.escape(section)}.*?)(?=\n## |\Z)"
521
+ new_section = f"## {section}\n{content}\n"
522
+
523
+ if re.search(section_pattern, current, re.DOTALL):
524
+ updated = re.sub(section_pattern, new_section, current, flags=re.DOTALL)
525
+ else:
526
+ updated = current.rstrip() + f"\n\n{new_section}"
527
+
528
+ # Add timestamp
529
+ updated = re.sub(
530
+ r"\*Last updated:.*\*",
531
+ f"*Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M')}*",
532
+ updated
533
+ )
534
+
535
+ memory_file.write_text(updated, encoding="utf-8")
536
+
537
+ except Exception as e:
538
+ logger.error(f"Failed to update working memory: {e}")
539
+
540
+ # =========================================================================
541
+ # DAILY LOG
542
+ # =========================================================================
543
+
544
+ async def log_to_daily(self, entry: str, category: str = "note"):
545
+ """Add entry to today's daily log."""
546
+ today = datetime.now().strftime("%Y-%m-%d")
547
+ daily_file = MEMORY_PATH / "daily" / f"{today}.md"
548
+
549
+ try:
550
+ if not daily_file.exists():
551
+ daily_file.parent.mkdir(exist_ok=True)
552
+ daily_file.write_text(f"# Daily Log - {today}\n\n")
553
+
554
+ timestamp = datetime.now().strftime("%H:%M")
555
+ with open(daily_file, "a", encoding="utf-8") as f:
556
+ f.write(f"- [{timestamp}] [{category}] {entry}\n")
557
+
558
+ except Exception as e:
559
+ logger.error(f"Failed to write daily log: {e}")
560
+
561
+
562
+ # ============================================================================
563
+ # SINGLETON
564
+ # ============================================================================
565
+
566
+ _memory_manager: Optional[MemoryManager] = None
567
+
568
+
569
+ async def get_memory_manager(db_pool=None) -> MemoryManager:
570
+ """Get or create memory manager singleton."""
571
+ global _memory_manager
572
+ if _memory_manager is None:
573
+ _memory_manager = MemoryManager(db_pool)
574
+ await _memory_manager.initialize()
575
+ return _memory_manager
576
+
577
+
578
+ def get_memory_manager_sync() -> MemoryManager:
579
+ """Synchronous getter - assumes already initialized."""
580
+ global _memory_manager
581
+ if _memory_manager is None:
582
+ _memory_manager = MemoryManager()
583
+ asyncio.run(_memory_manager.initialize())
584
+ return _memory_manager