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
hyperclaw/persona.py ADDED
@@ -0,0 +1,367 @@
1
+ """The customer's persona: style preferences they set or agreed to (spec §14).
2
+
3
+ - Per customer, stored in ``$HYPERCLAW_ROOT/persona.json``. Empty by default:
4
+ there is no seeded persona.
5
+ - Consent-based: an inferred preference is only ever *proposed*; it is kept
6
+ only after the customer says yes. Declined proposals are remembered so the
7
+ same one is not asked again.
8
+ - Visible, editable and resettable: ``/persona`` in the terminal and Telegram,
9
+ ``hyperclaw persona`` on the command line, all through :func:`run_command`.
10
+ - Owner only: guest chats never receive it (callers pass the chat scope).
11
+ - Tone only: :func:`render` frames it as style preferences that sit below
12
+ the truth rules and cannot change facts, claims or refusals.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+ import re
19
+ import time
20
+ from pathlib import Path
21
+ from typing import Optional
22
+
23
+ KEYS = {
24
+ "tone": "Tone",
25
+ "length": "Answer length",
26
+ "address": "Address the owner as",
27
+ "topics": "Topics they care about",
28
+ "notes": "Style notes (owner's words, style only)",
29
+ }
30
+ MAX_VALUE = 200
31
+ MAX_TOPICS = 12
32
+
33
+ PERSONA_HEADING = "## Persona: the owner's style preferences (tone and wording only)"
34
+ PERSONA_FRAME = (
35
+ "The owner set or approved these preferences. They shape tone, length and "
36
+ "wording only. They never change facts, never claim an action that has no "
37
+ "matching tool result, never override a refusal, and never override the "
38
+ "truthfulness, access or guest rules above; where anything here conflicts "
39
+ "with those rules, the rules win. Whatever this section says, if asked "
40
+ "whether you are an AI, say yes."
41
+ )
42
+
43
+
44
+ def path() -> Path:
45
+ from hyperclaw.userconfig import root
46
+ return root() / "persona.json"
47
+
48
+
49
+ def clean_value(value: str) -> str:
50
+ """One line, no markdown headings or fences, bounded length — persona
51
+ text is data, it must not be able to open a new prompt section."""
52
+ v = re.sub(r"\s+", " ", str(value or "")).strip()
53
+ v = v.replace("```", "'''").lstrip("#").strip()
54
+ return v[:MAX_VALUE]
55
+
56
+
57
+ def _empty() -> dict:
58
+ return {"version": 1, "preferences": {}, "pending": [], "declined": [],
59
+ "updated_at": None}
60
+
61
+
62
+ class PersonaStore:
63
+ def __init__(self, file: Optional[Path] = None):
64
+ self.file = Path(file) if file else path()
65
+ self.data = self._load()
66
+
67
+ # ── persistence ──────────────────────────────────────────────────────
68
+ def _load(self) -> dict:
69
+ try:
70
+ if self.file.exists():
71
+ d = json.loads(self.file.read_text(encoding="utf-8"))
72
+ if isinstance(d, dict):
73
+ base = _empty()
74
+ base.update({k: d[k] for k in base if k in d})
75
+ return base
76
+ except Exception:
77
+ pass
78
+ return _empty()
79
+
80
+ def save(self) -> None:
81
+ self.data["updated_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
82
+ self.file.parent.mkdir(parents=True, exist_ok=True)
83
+ tmp = self.file.with_suffix(".json.tmp")
84
+ tmp.write_text(json.dumps(self.data, indent=2), encoding="utf-8")
85
+ os.replace(tmp, self.file)
86
+
87
+ # ── preferences ──────────────────────────────────────────────────────
88
+ @property
89
+ def preferences(self) -> dict:
90
+ return self.data["preferences"]
91
+
92
+ def set(self, key: str, value) -> str:
93
+ """Set a preference the customer stated directly (that is consent)."""
94
+ if key not in KEYS:
95
+ raise KeyError(key)
96
+ if key == "topics":
97
+ items = value if isinstance(value, list) else str(value).split(",")
98
+ topics = [clean_value(t)[:60] for t in items if clean_value(t)]
99
+ if not topics:
100
+ raise ValueError("no topics given")
101
+ self.preferences["topics"] = topics[:MAX_TOPICS]
102
+ shown = ", ".join(self.preferences["topics"])
103
+ else:
104
+ v = clean_value(value)
105
+ if not v:
106
+ raise ValueError("empty value")
107
+ self.preferences[key] = v
108
+ shown = v
109
+ self.save()
110
+ return shown
111
+
112
+ def unset(self, key: str) -> bool:
113
+ had = self.preferences.pop(key, None) is not None
114
+ if had:
115
+ self.save()
116
+ return had
117
+
118
+ def reset(self) -> None:
119
+ """Back to the default: no preferences, nothing pending, nothing declined."""
120
+ self.data = _empty()
121
+ self.save()
122
+
123
+ # ── consent for inferred preferences ─────────────────────────────────
124
+ def _same(self, a: dict, b: dict) -> bool:
125
+ return a.get("key") == b.get("key") and \
126
+ str(a.get("value")).lower() == str(b.get("value")).lower()
127
+
128
+ def propose(self, key: str, value: str, evidence: str = "") -> Optional[dict]:
129
+ """Queue an inferred preference for the customer's yes/no. Nothing is
130
+ applied. Returns the pending item, or None if it is already set,
131
+ already pending or was declined before."""
132
+ if key not in KEYS:
133
+ return None
134
+ value = clean_value(value)
135
+ if not value:
136
+ return None
137
+ cand = {"key": key, "value": value}
138
+ cur = self.preferences.get(key)
139
+ if (isinstance(cur, list) and value.lower() in [c.lower() for c in cur]) or \
140
+ (isinstance(cur, str) and cur.lower() == value.lower()):
141
+ return None
142
+ if any(self._same(cand, p) for p in self.data["pending"] + self.data["declined"]):
143
+ return None
144
+ nid = max([p.get("id", 0) for p in self.data["pending"]] + [0]) + 1
145
+ item = {"id": nid, "key": key, "value": value,
146
+ "evidence": clean_value(evidence)[:120]}
147
+ self.data["pending"].append(item)
148
+ self.save()
149
+ return item
150
+
151
+ def pending(self) -> list:
152
+ return list(self.data["pending"])
153
+
154
+ def _take(self, pid: Optional[int]) -> Optional[dict]:
155
+ items = self.data["pending"]
156
+ if not items:
157
+ return None
158
+ item = items[-1] if pid is None else next((p for p in items if p.get("id") == pid), None)
159
+ if item:
160
+ items.remove(item)
161
+ return item
162
+
163
+ def accept(self, pid: Optional[int] = None) -> Optional[dict]:
164
+ item = self._take(pid)
165
+ if not item:
166
+ return None
167
+ if item["key"] == "topics":
168
+ topics = list(self.preferences.get("topics") or [])
169
+ topics.append(item["value"])
170
+ self.preferences["topics"] = topics[-MAX_TOPICS:]
171
+ else:
172
+ self.preferences[item["key"]] = item["value"]
173
+ self.save()
174
+ return item
175
+
176
+ def decline(self, pid: Optional[int] = None) -> Optional[dict]:
177
+ item = self._take(pid)
178
+ if not item:
179
+ return None
180
+ self.data["declined"].append({"key": item["key"], "value": item["value"]})
181
+ self.save()
182
+ return item
183
+
184
+
185
+ # ── inference (deterministic, conservative) ─────────────────────────────
186
+
187
+ _ANS = r"(?:(?:your|the|my|all|these|those)\s+)?(?:answers?|repl(?:y|ies)|responses?|messages?|texts?)"
188
+ _RULES = [
189
+ # "keep your answers short", "keep it brief", "make your replies shorter",
190
+ # "shorter answers please", "be concise", "less wordy", "too long", "tl;dr"
191
+ (re.compile(r"\b(keep (it|things|" + _ANS + r") (short|brief|concise|tight|to the point)|"
192
+ r"(make|give me|i want|i'd like|i prefer|please give)\s+(it|" + _ANS + r"|me)?\s*"
193
+ r"(short(er)?|brief(er)?|concise)\b(?!\s+(summary|version) of)|"
194
+ r"be (more )?(brief|concise|succinct)|short(er)? (answers|replies|responses)|"
195
+ r"(answers?|repl(y|ies)|responses?) (are|is) too long|too long|less (wordy|verbose)|"
196
+ r"(don't|do not|stop) ramble|fewer words)\b", re.I), "length", "short"),
197
+ (re.compile(r"\b(more detail(ed)?|longer (answers|replies|responses)|be (more )?(thorough|detailed)|"
198
+ r"(give me|i want|i'd like|i prefer) (more )?(detailed|longer|in-depth) "
199
+ r"(answers|replies|responses)|go (into )?more (depth|detail))\b", re.I),
200
+ "length", "detailed"),
201
+ (re.compile(r"\b(be|sound|keep it|talk) (more )?(casual|relaxed|informal|laid[- ]back)\b", re.I),
202
+ "tone", "casual"),
203
+ (re.compile(r"\b(be|sound|keep it|talk) (more )?(formal|professional)\b", re.I), "tone", "formal"),
204
+ (re.compile(r"\b(be|sound|keep it|talk) (more )?(warm|warmer|friendly|friendlier)\b", re.I),
205
+ "tone", "warm"),
206
+ (re.compile(r"\b(be|sound|keep it|talk) (more )?(direct|blunt|straight)\b|"
207
+ r"\b(no|skip the|cut the) (fluff|small talk|pleasantries)\b", re.I), "tone", "direct"),
208
+ ]
209
+ # "call me Sam", "call me Dr. Lee from now on": the first word, then any
210
+ # following Capitalised words (so "Dr. Lee" survives its period).
211
+ _ADDRESS = re.compile(r"\b(?:call me|address me as)\s+([A-Za-z][\w'.-]*(?:\s+[A-Z][\w'.-]*){0,3})",
212
+ re.I)
213
+ _TOPIC = re.compile(r"\bi(?:'m| am) (?:really |very )?interested in ([^.!?\n]{2,40})", re.I)
214
+
215
+
216
+ def infer(message: str) -> list:
217
+ """Candidate (key, value) preferences stated in an owner message."""
218
+ out = []
219
+ text = message or ""
220
+ for rx, key, value in _RULES:
221
+ if rx.search(text):
222
+ out.append((key, value))
223
+ break
224
+ m = _ADDRESS.search(text)
225
+ if m:
226
+ # re.I makes [A-Z] match lowercase too; keep only Capitalised follow-ons.
227
+ words = m.group(1).split()
228
+ kept = words[:1]
229
+ for w in words[1:]:
230
+ if not w[:1].isupper():
231
+ break
232
+ kept.append(w)
233
+ out.append(("address", " ".join(kept).rstrip(".,")))
234
+ m = _TOPIC.search(text)
235
+ if m:
236
+ out.append(("topics", m.group(1).strip()))
237
+ return out
238
+
239
+
240
+ def describe(item: dict) -> str:
241
+ key, value = item["key"], item["value"]
242
+ return {
243
+ "length": f"you prefer {value} answers",
244
+ "tone": f"you prefer a {value} tone",
245
+ "address": f"you'd like to be called {value}",
246
+ "topics": f"you're interested in {value}",
247
+ "notes": f"style note: {value}",
248
+ }.get(key, f"{key}: {value}")
249
+
250
+
251
+ def consent_question(item: dict) -> str:
252
+ return f"Want me to remember that {describe(item)}? (yes/no)"
253
+
254
+
255
+ def observe(message: str, store: Optional[PersonaStore] = None) -> Optional[dict]:
256
+ """Owner surfaces call this per owner message. Returns at most one new
257
+ pending item to ask about, or None. Never applies anything."""
258
+ store = store or PersonaStore()
259
+ for key, value in infer(message):
260
+ item = store.propose(key, value, evidence=message)
261
+ if item:
262
+ return item
263
+ return None
264
+
265
+
266
+ YES = {"y", "yes", "yeah", "yep", "sure", "ok", "okay", "please do", "remember it"}
267
+ NO = {"n", "no", "nope", "don't", "dont", "no thanks", "forget it"}
268
+
269
+
270
+ def answer_kind(text: str) -> str:
271
+ t = (text or "").strip().lower().rstrip(".!")
272
+ return "yes" if t in YES else "no" if t in NO else ""
273
+
274
+
275
+ # ── rendering ───────────────────────────────────────────────────────────
276
+
277
+ def render(store: Optional[PersonaStore] = None) -> str:
278
+ """The persona section for an OWNER system prompt; "" if nothing is set.
279
+ Callers place it after TRUTH_RULES and the access manifest."""
280
+ store = store or PersonaStore()
281
+ prefs = store.preferences
282
+ if not prefs:
283
+ return ""
284
+ lines = [PERSONA_HEADING, PERSONA_FRAME]
285
+ for key, label in KEYS.items():
286
+ v = prefs.get(key)
287
+ if not v:
288
+ continue
289
+ if isinstance(v, list):
290
+ v = ", ".join(v)
291
+ lines.append(f'- {label}: "{v}"')
292
+ return "\n".join(lines)
293
+
294
+
295
+ # ── commands (TUI /persona, Telegram /persona, `hyperclaw persona`) ─────
296
+
297
+ HELP = (
298
+ "Persona commands:\n"
299
+ " /persona show what shapes my style\n"
300
+ " /persona set <key> <value> keys: tone, length, address, topics, notes\n"
301
+ " /persona unset <key> remove one preference\n"
302
+ " /persona pending preferences waiting for your yes/no\n"
303
+ " /persona accept [id] keep a pending preference\n"
304
+ " /persona decline [id] drop a pending preference\n"
305
+ " /persona reset clear everything back to the default"
306
+ )
307
+
308
+
309
+ def show(store: PersonaStore) -> str:
310
+ prefs = store.preferences
311
+ if not prefs:
312
+ body = "No style preferences saved. I use the default style."
313
+ else:
314
+ rows = []
315
+ for key, label in KEYS.items():
316
+ v = prefs.get(key)
317
+ if v:
318
+ rows.append(f"- {key} ({label}): {', '.join(v) if isinstance(v, list) else v}")
319
+ body = "Saved style preferences:\n" + "\n".join(rows)
320
+ pend = store.pending()
321
+ if pend:
322
+ body += "\nWaiting for your yes/no:\n" + "\n".join(
323
+ f" [{p['id']}] {describe(p)}" for p in pend)
324
+ return body + "\nThese only shape tone and wording; they never change facts."
325
+
326
+
327
+ def run_command(argv: list, store: Optional[PersonaStore] = None) -> str:
328
+ store = store or PersonaStore()
329
+ if not argv or argv[0] in ("show", "view", "list"):
330
+ return show(store)
331
+ cmd, rest = argv[0].lower(), argv[1:]
332
+ if cmd in ("help", "-h", "--help"):
333
+ return HELP
334
+ if cmd in ("set", "edit"):
335
+ if len(rest) < 2 or rest[0].lower() not in KEYS:
336
+ return "Usage: /persona set <tone|length|address|topics|notes> <value>"
337
+ key = rest[0].lower()
338
+ try:
339
+ shown = store.set(key, " ".join(rest[1:]))
340
+ except ValueError:
341
+ return "That value is empty; nothing saved."
342
+ return f"Saved {key}: {shown}"
343
+ if cmd in ("unset", "remove", "forget"):
344
+ if not rest or rest[0].lower() not in KEYS:
345
+ return "Usage: /persona unset <tone|length|address|topics|notes>"
346
+ return (f"Removed {rest[0].lower()}." if store.unset(rest[0].lower())
347
+ else f"No {rest[0].lower()} was saved.")
348
+ if cmd == "reset":
349
+ store.reset()
350
+ return "Persona reset to the default. No style preferences are saved."
351
+ if cmd == "pending":
352
+ pend = store.pending()
353
+ return ("Nothing is waiting for your yes/no." if not pend else
354
+ "\n".join(f"[{p['id']}] {describe(p)}" for p in pend))
355
+ if cmd in ("accept", "decline"):
356
+ pid = None
357
+ if rest:
358
+ try:
359
+ pid = int(rest[0])
360
+ except ValueError:
361
+ return f"Usage: /persona {cmd} [id]"
362
+ item = store.accept(pid) if cmd == "accept" else store.decline(pid)
363
+ if not item:
364
+ return "Nothing pending with that id."
365
+ return (f"Saved: {describe(item)}." if cmd == "accept"
366
+ else f"OK, I won't keep that ({describe(item)}).")
367
+ return HELP
@@ -0,0 +1,219 @@
1
+ """
2
+ HyperClaw Memory Agent
3
+ Processes daily logs into summarized knowledge.
4
+ Runs on schedule or on-demand via /api/memory/consolidate.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import logging
10
+ import os
11
+ import re
12
+ from datetime import datetime, timedelta
13
+ from pathlib import Path
14
+ from typing import Optional
15
+
16
+ import anthropic
17
+ from hyperclaw.api_utils import extract_text
18
+ from hyperclaw.cost_guard import max_output
19
+ from dotenv import load_dotenv
20
+
21
+ load_dotenv()
22
+
23
+ logger = logging.getLogger("hyperclaw.memory")
24
+
25
+ # Paths - use environment variable or default
26
+ HYPERCLAW_ROOT = Path(os.environ.get("HYPERCLAW_ROOT", Path.home() / ".hyperclaw"))
27
+ MEMORY_PATH = HYPERCLAW_ROOT / "memory"
28
+ WORKSPACE_PATH = HYPERCLAW_ROOT / "workspace"
29
+ KNOWLEDGE_FILE = MEMORY_PATH / "KNOWLEDGE.md"
30
+ SUMMARY_FILE = MEMORY_PATH / "summary.md"
31
+
32
+ MODEL = os.environ.get("HYPERCLAW_MODEL", "claude-sonnet-4-6")
33
+ MAX_TOKENS = 4096 # default of cost_guard path "memory" (config: max_output_tokens_per_path)
34
+
35
+
36
+ class MemoryAgent:
37
+ """Memory consolidation engine - summarizes daily logs."""
38
+
39
+ def __init__(self):
40
+ from hyperclaw.cost_guard import guard_client
41
+ self.client = guard_client(
42
+ anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY", "")), "anthropic")
43
+ self._last_run: Optional[datetime] = None
44
+
45
+ async def consolidate(self, days_back: int = 1) -> dict:
46
+ """
47
+ Main consolidation routine.
48
+ 1. Read recent daily logs
49
+ 2. Extract key facts and decisions
50
+ 3. Update knowledge file
51
+ """
52
+ logger.info(f"Memory consolidation starting (days_back={days_back})")
53
+
54
+ results = {
55
+ "logs_processed": 0,
56
+ "facts_extracted": 0,
57
+ "errors": [],
58
+ }
59
+
60
+ try:
61
+ logs = self._gather_logs(days_back)
62
+ results["logs_processed"] = len(logs)
63
+
64
+ if not logs:
65
+ logger.info("No logs to process")
66
+ return results
67
+
68
+ facts = await self._extract_facts(logs)
69
+ if facts:
70
+ self._append_knowledge(facts)
71
+ results["facts_extracted"] = len(facts)
72
+
73
+ self._last_run = datetime.now()
74
+ logger.info(f"Memory consolidation complete: {results}")
75
+
76
+ except Exception as e:
77
+ logger.error(f"Consolidation error: {e}", exc_info=True)
78
+ results["errors"].append(str(e))
79
+
80
+ return results
81
+
82
+ def _gather_logs(self, days_back: int) -> list[dict]:
83
+ """Read recent daily logs."""
84
+ logs = []
85
+ today = datetime.now()
86
+
87
+ for i in range(days_back):
88
+ date = today - timedelta(days=i)
89
+ date_str = date.strftime("%Y-%m-%d")
90
+
91
+ for base in [MEMORY_PATH, WORKSPACE_PATH / "memory"]:
92
+ log_path = base / f"{date_str}.md"
93
+ if log_path.exists():
94
+ try:
95
+ content = log_path.read_text(encoding="utf-8")
96
+ if content.strip():
97
+ logs.append({
98
+ "date": date_str,
99
+ "path": str(log_path),
100
+ "content": content[:50000],
101
+ })
102
+ except Exception as e:
103
+ logger.warning(f"Failed to read {log_path}: {e}")
104
+
105
+ return logs
106
+
107
+ async def _extract_facts(self, logs: list[dict]) -> list[str]:
108
+ """Extract key facts and decisions from logs."""
109
+ combined = "\n\n---\n\n".join(
110
+ f"## {log['date']}\n{log['content']}" for log in logs
111
+ )
112
+
113
+ prompt = f"""Analyze these daily logs and extract KEY FACTS that should be remembered.
114
+
115
+ Extract:
116
+ - Decisions made
117
+ - New information learned
118
+ - Completed tasks
119
+ - Important changes
120
+
121
+ Format each as:
122
+ [YYYY-MM-DD] Brief description
123
+
124
+ Do NOT include:
125
+ - Routine tasks
126
+ - In-progress work
127
+ - Technical debugging
128
+
129
+ Return only extracted facts, one per line. If none, return "NONE".
130
+
131
+ LOGS:
132
+ {combined}"""
133
+
134
+ try:
135
+ response = await asyncio.to_thread(
136
+ self.client.messages.create,
137
+ model=MODEL,
138
+ max_tokens=max_output("memory", "anthropic", MODEL),
139
+ messages=[{"role": "user", "content": prompt}],
140
+ )
141
+ text = extract_text(response)
142
+
143
+ if text == "NONE":
144
+ return []
145
+
146
+ facts = [
147
+ line.strip() for line in text.split("\n")
148
+ if line.strip() and line.strip().startswith("[")
149
+ ]
150
+ return facts
151
+ except Exception as e:
152
+ logger.error(f"Fact extraction failed: {e}")
153
+ return []
154
+
155
+ def _append_knowledge(self, facts: list[str]) -> None:
156
+ """Append new facts to knowledge file."""
157
+ if not facts:
158
+ return
159
+
160
+ KNOWLEDGE_FILE.parent.mkdir(parents=True, exist_ok=True)
161
+
162
+ existing = ""
163
+ if KNOWLEDGE_FILE.exists():
164
+ existing = KNOWLEDGE_FILE.read_text(encoding="utf-8")
165
+
166
+ new_facts = []
167
+ for fact in facts:
168
+ match = re.match(r"\[(\d{4}-\d{2}-\d{2})\]\s*(.+)", fact)
169
+ if match:
170
+ content = match.group(2).lower()
171
+ if content not in existing.lower():
172
+ new_facts.append(fact)
173
+
174
+ if new_facts:
175
+ with open(KNOWLEDGE_FILE, "a", encoding="utf-8") as f:
176
+ f.write("\n")
177
+ for fact in new_facts:
178
+ f.write(f"- {fact}\n")
179
+ logger.info(f"Added {len(new_facts)} facts to KNOWLEDGE.md")
180
+
181
+ def status(self) -> dict:
182
+ """Get agent status."""
183
+ return {
184
+ "name": "memory_agent",
185
+ "status": "active",
186
+ "last_run": self._last_run.isoformat() if self._last_run else None,
187
+ "knowledge_file": str(KNOWLEDGE_FILE),
188
+ }
189
+
190
+
191
+ # Singleton
192
+ _agent: Optional[MemoryAgent] = None
193
+
194
+
195
+ def get_memory_agent() -> MemoryAgent:
196
+ """Get or create memory agent singleton."""
197
+ global _agent
198
+ if _agent is None:
199
+ _agent = MemoryAgent()
200
+ return _agent
201
+
202
+
203
+ async def run_consolidation(days_back: int = 1) -> dict:
204
+ """Run memory consolidation."""
205
+ agent = get_memory_agent()
206
+ return await agent.consolidate(days_back)
207
+
208
+
209
+ # Backwards compatibility
210
+ Prometheus = MemoryAgent
211
+ get_prometheus = get_memory_agent
212
+
213
+
214
+ if __name__ == "__main__":
215
+ import sys
216
+ logging.basicConfig(level=logging.INFO)
217
+ days = int(sys.argv[1]) if len(sys.argv) > 1 else 1
218
+ result = asyncio.run(run_consolidation(days))
219
+ print(json.dumps(result, indent=2))