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/solomon.py ADDED
@@ -0,0 +1,442 @@
1
+ """
2
+ HyperClaw Chat Agent
3
+ Simple chat interface used by Telegram bot and scheduler (chat-only, no tools).
4
+ Every prompt is built for a chat scope (hyperclaw.chat_scope): GUEST unless
5
+ the caller proves the chat is the owner's.
6
+ Users can customize the AI name and personality via workspace files.
7
+ """
8
+
9
+ import logging
10
+ import os
11
+ import asyncio
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+ from typing import AsyncIterator, Optional
15
+
16
+ import anthropic
17
+ from hyperclaw.api_utils import extract_text
18
+ from dotenv import load_dotenv
19
+
20
+ from hyperclaw import cost_guard
21
+ from hyperclaw.chat_scope import GUEST, GUEST_RULES, OWNER
22
+ from hyperclaw.cost_guard import guard_client
23
+ from hyperclaw.chat_scope import normalize as normalize_scope
24
+
25
+ _OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "")
26
+ _USE_OPENAI_COMPAT = bool(_OPENAI_API_BASE)
27
+
28
+ load_dotenv()
29
+
30
+ # Paths - use environment variable or default to ~/.hyperclaw
31
+ HYPERCLAW_ROOT = Path(os.environ.get("HYPERCLAW_ROOT", Path.home() / ".hyperclaw"))
32
+ WORKSPACE_PATH = HYPERCLAW_ROOT / "workspace"
33
+ MEMORY_PATH = HYPERCLAW_ROOT / "memory"
34
+
35
+ MODEL = os.environ.get("OPENAI_MODEL") or os.environ.get("HYPERCLAW_MODEL", "claude-sonnet-4-6")
36
+ # Legacy override for the chat-reply max output; the per-path setting is
37
+ # config.json cost_guard.max_output_tokens_per_path["telegram"] (default 4096).
38
+ MAX_TOKENS = int(os.environ.get("HYPERCLAW_MAX_TOKENS", 4096))
39
+ MAX_HISTORY = 20
40
+ MAX_OUTPUT_PATH = "telegram"
41
+
42
+
43
+ def chat_max_tokens(provider: str, model: str) -> int:
44
+ """The explicit max output for one chat reply (cost guard: every path sets
45
+ one, within the per-task cap for this model)."""
46
+ env = os.environ.get("HYPERCLAW_MAX_TOKENS", "").strip()
47
+ return cost_guard.max_output(MAX_OUTPUT_PATH, provider, model,
48
+ requested=int(env) if env.isdigit() else None)
49
+
50
+
51
+ def chat_thinking_on() -> bool:
52
+ """Every chat answer streams its reasoning, Ask included (Vaughn, 7 Oct 2026,
53
+ item 12: "My live-reasoning standard applies to every answer, Ask included").
54
+ HYPERCLAW_CHAT_THINKING=off is the switch that turns it off."""
55
+ return os.environ.get("HYPERCLAW_CHAT_THINKING", "on").strip().lower() != "off"
56
+
57
+
58
+ def chat_request_kwargs(prov_kind: str, model: str, method=None, provider: str = "") -> dict:
59
+ """Request kwargs for one chat turn on this provider.
60
+
61
+ Messages-API providers: max_tokens plus the model's thinking parameters from
62
+ anthropic_params (the one place that decides them), at low effort for these
63
+ Ask-style chat answers; with the switch off, max_tokens only.
64
+ OpenAI-compatible providers (HyperSpeed V2 on vLLM reasons by default): nothing
65
+ extra while on; with the switch off, the chat-template switch that stops it.
66
+ Hosted OpenAI-schema providers ("openai") get max_tokens only.
67
+ """
68
+ mx = chat_max_tokens(provider or prov_kind, model)
69
+ if prov_kind == "anthropic":
70
+ from hyperclaw.anthropic_params import as_sdk_kwargs, thinking_params
71
+ if not chat_thinking_on():
72
+ return {"max_tokens": mx}
73
+ params = thinking_params(model, mx, overrides={"effort": "low"})
74
+ return as_sdk_kwargs(params, method) if method is not None else params
75
+ kwargs: dict = {"max_tokens": mx}
76
+ if prov_kind == "openai_compat" and not chat_thinking_on():
77
+ kwargs["extra_body"] = {"chat_template_kwargs": {"enable_thinking": False}}
78
+ return kwargs
79
+
80
+ # Context files to load (in order, skip if missing)
81
+ CONTEXT_FILES = [
82
+ "ASSISTANT.md", # AI personality and name
83
+ "USER.md", # User preferences
84
+ "MEMORY.md", # Working memory
85
+ ]
86
+
87
+
88
+ def _get_identity() -> tuple:
89
+ """(ai_name, user_name) — onboarding writes ~/.hyperclaw/config.json;
90
+ older builds used config/settings.json. Check both, in that order."""
91
+ import json
92
+ for cf in (HYPERCLAW_ROOT / "config.json",
93
+ HYPERCLAW_ROOT / "config" / "settings.json"):
94
+ if cf.exists():
95
+ try:
96
+ config = json.loads(cf.read_text())
97
+ return (config.get("ai_name") or os.environ.get("HYPERCLAW_AI_NAME", "Assistant"),
98
+ config.get("user_name") or "")
99
+ except Exception:
100
+ continue
101
+ return (os.environ.get("HYPERCLAW_AI_NAME", "Assistant"), "")
102
+
103
+
104
+ def _get_ai_name() -> str:
105
+ return _get_identity()[0]
106
+
107
+
108
+ def _model_family(model: str) -> str:
109
+ """Human-readable family for a model id, or "" if unknown."""
110
+ m = model.lower()
111
+ if "hyperspeed" in m or m.startswith("hyper-nimbus/"):
112
+ return "HyperAI"
113
+ if "claude" in m:
114
+ return "Claude"
115
+ return ""
116
+
117
+
118
+ class ChatAgent:
119
+ """Chat agent - manages conversation and context."""
120
+
121
+ def __init__(self):
122
+ if _USE_OPENAI_COMPAT:
123
+ import openai
124
+ self._oai = guard_client(openai.AsyncOpenAI(
125
+ api_key=os.environ.get("OPENAI_API_KEY", ""),
126
+ base_url=_OPENAI_API_BASE,
127
+ ))
128
+ self.client = None
129
+ else:
130
+ self._oai = None
131
+ self.client = guard_client(
132
+ anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY", "")), "anthropic")
133
+ self.ai_name, self.user_name = _get_identity()
134
+ self.system_prompt = self._load_system_prompt()
135
+
136
+ def _load_system_prompt(self, model: str = "", scope: str = GUEST) -> str:
137
+ """Build the system prompt (hyperclaw.prompt_assembly order).
138
+
139
+ model: the model id actually serving the conversation; defaults to
140
+ the configured MODEL so callers routing to another provider can
141
+ rebuild the prompt with the real model.
142
+ scope: chat_scope.OWNER or GUEST. Anything but OWNER gets the guest
143
+ prompt: no persona, no standing instructions, no USER/MEMORY files,
144
+ no daily log, no owner name (spec §6 guest fence; benchmark 4.1).
145
+ Truth rules and the access manifest always come first (spec §14)."""
146
+ from hyperclaw.prompt_assembly import assemble, identity_block, persona_for
147
+ from hyperclaw.truth_guard import TRUTH_RULES, access_manifest
148
+
149
+ model = model or MODEL
150
+ scope = normalize_scope(scope)
151
+ owner = scope == OWNER
152
+ identity = identity_block(self.ai_name, self.user_name if owner else "",
153
+ scope, model=model, family=_model_family(model))
154
+ surface = "\n".join([
155
+ "## Core Behaviors",
156
+ "- Be helpful, accurate, and concise",
157
+ "- Execute tasks proactively when given clear instructions",
158
+ "- Ask clarifying questions when needed",
159
+ "- Be honest about limitations",
160
+ "",
161
+ f"Current date: {datetime.now().strftime('%Y-%m-%d %H:%M')}",
162
+ ])
163
+ access = access_manifest("chat_only", surface="telegram", scope=scope)
164
+ if not owner:
165
+ # Guest fence: nothing from the owner's workspace or memory.
166
+ return assemble(truth_rules=TRUTH_RULES, access=access, identity=identity,
167
+ scope=scope, surface_instructions=surface)
168
+
169
+ # Legacy persona file from older onboarding: kept as the owner's
170
+ # standing instructions, below the truth rules.
171
+ standing = ""
172
+ claude_md = HYPERCLAW_ROOT / "CLAUDE.md"
173
+ if claude_md.exists():
174
+ try:
175
+ standing = claude_md.read_text(encoding="utf-8")
176
+ except Exception:
177
+ standing = ""
178
+
179
+ context = []
180
+ for filename in CONTEXT_FILES:
181
+ filepath = WORKSPACE_PATH / filename
182
+ if filepath.exists():
183
+ try:
184
+ content = filepath.read_text(encoding="utf-8")
185
+ context.append(f"## {filename}\n{content}")
186
+ except Exception:
187
+ pass
188
+
189
+ today = datetime.now().strftime("%Y-%m-%d")
190
+ log_path = MEMORY_PATH / f"{today}.md"
191
+ if log_path.exists():
192
+ try:
193
+ content = log_path.read_text(encoding="utf-8")
194
+ context.append(f"## Today's Log ({today})\n{content}")
195
+ except Exception:
196
+ pass
197
+
198
+ return assemble(truth_rules=TRUTH_RULES, access=access, identity=identity,
199
+ scope=scope, surface_instructions=surface,
200
+ persona=persona_for(scope), standing=standing, context=context)
201
+
202
+ async def chat(self, message: str, history: list[dict], scope: str = GUEST,
203
+ unattended: Optional[bool] = None) -> str:
204
+ """Send a message and get a response. scope defaults to GUEST
205
+ (fail closed); owner-only callers such as the scheduler pass OWNER.
206
+
207
+ Goes through the provider registry like stream_events, so the
208
+ scheduler works on a HyperSpeed V2-only install and honours a
209
+ Hyper-deployed install's V2-only rule.
210
+
211
+ unattended: a job with nobody waiting on the answer (the scheduler).
212
+ None means "owner scope with no conversation", which is how the
213
+ scheduler calls this. At a cost cap an unattended job stops and
214
+ returns a notice for the owner only (scheduler output goes only to
215
+ the owner's chat); it never gets a cheaper model."""
216
+ if unattended is None:
217
+ unattended = normalize_scope(scope) == OWNER and not history
218
+ out = []
219
+ try:
220
+ with cost_guard.task(surface="scheduler" if unattended else "chat", request=message,
221
+ unattended=unattended, owner=normalize_scope(scope) == OWNER):
222
+ async for kind, chunk in self.stream_events(message, history, scope=scope):
223
+ if kind == "text":
224
+ out.append(chunk)
225
+ except Exception as e:
226
+ # In-band marker: consumers strip it (strip_internal_errors) and
227
+ # show a notices.py failure notice; the detail is logged here.
228
+ logging.getLogger("hyperclaw.solomon").error("chat failed: %s", e, exc_info=True)
229
+ return f"[Error: {e}]"
230
+ return "".join(out)
231
+
232
+ async def _attempt_stream(self, prov, model, messages,
233
+ scope: str = GUEST) -> AsyncIterator[tuple]:
234
+ """One provider attempt for stream_events — yields ("thinking"|"text", delta)."""
235
+ system = self._load_system_prompt(model, scope=scope)
236
+ if prov.kind == "anthropic":
237
+ from hyperclaw.model_catalog import TELEGRAM, is_selected
238
+ if not is_selected(TELEGRAM, getattr(prov, "name", ""), model): # /model wins
239
+ model = os.environ.get("HYPERCLAW_MODEL", "").strip() or model
240
+ async_client = guard_client(anthropic.AsyncAnthropic(api_key=prov.api_key),
241
+ getattr(prov, "name", None))
242
+ async with async_client.messages.stream(
243
+ model=model,
244
+ system=system,
245
+ messages=messages,
246
+ **chat_request_kwargs(prov.kind, model, async_client.messages.stream,
247
+ provider=getattr(prov, "name", "") or ""),
248
+ ) as stream:
249
+ async for event in stream:
250
+ if event.type == "content_block_delta":
251
+ dtype = getattr(event.delta, "type", "")
252
+ if dtype == "thinking_delta":
253
+ yield ("thinking", event.delta.thinking)
254
+ elif dtype == "text_delta":
255
+ yield ("text", event.delta.text)
256
+ return
257
+ # openai_compat / openai: chat-completions schema, text content only
258
+ from openai import AsyncOpenAI
259
+ client = guard_client(AsyncOpenAI(api_key=prov.api_key, base_url=prov.base_url or None),
260
+ getattr(prov, "name", None))
261
+ oai = [{"role": "system", "content": system}]
262
+ for m in messages:
263
+ c = m.get("content")
264
+ if isinstance(c, list):
265
+ c = "\n".join(b.get("text", "") for b in c
266
+ if isinstance(b, dict) and b.get("type") == "text")
267
+ if c:
268
+ oai.append({"role": m["role"], "content": c})
269
+ from hyperclaw.xai import clean_request
270
+ stream = await client.chat.completions.create(
271
+ model=model, messages=oai, stream=True,
272
+ **clean_request(getattr(prov, "name", ""), getattr(prov, "base_url", ""), model,
273
+ chat_request_kwargs(prov.kind, model,
274
+ provider=getattr(prov, "name", "") or "")),
275
+ )
276
+ # Reasoning (vLLM reasoning_content / inline <think>) -> "thinking",
277
+ # answer -> "text" with its leading blank lines dropped.
278
+ from hyperclaw.oai_stream import DeltaSplitter
279
+ splitter = DeltaSplitter()
280
+ async for chunk in stream:
281
+ if not chunk.choices:
282
+ continue # e.g. the trailing usage-only chunk
283
+ # reasoning_content / reasoning -> ("thinking", ...); content -> ("text", ...)
284
+ for item in splitter.feed(chunk.choices[0].delta):
285
+ yield item
286
+ for item in splitter.flush():
287
+ yield item
288
+
289
+ async def stream_events(self, message: str, history: list[dict],
290
+ attachments: list | None = None,
291
+ scope: str = GUEST, _unattended: bool = False) -> AsyncIterator[tuple]:
292
+ """Stream ("thinking", delta) and ("text", delta) tuples.
293
+
294
+ Routes by capability: plain text turns use the `primary` slot,
295
+ attachment turns need documents/images and use `vision`. Failover
296
+ walks the slot ladder before the first token; a mid-stream death is
297
+ marked, never re-answered.
298
+
299
+ One call is one task for the cost guard (spec §9). At a cap no model
300
+ call is made: the owner gets the stop-and-ask message, a guest a
301
+ neutral reply (the owner is notified), an unattended job a notice
302
+ for the owner. Owner answers above the threshold end with a spend line.
303
+ """
304
+ owner = normalize_scope(scope) == OWNER
305
+ with cost_guard.task(surface="chat", request=message, owner=owner,
306
+ unattended=_unattended) as task:
307
+ if owner and not task.unattended:
308
+ notices = cost_guard.take_owner_notices()
309
+ if notices:
310
+ yield ("text", "\n".join(notices) + "\n\n")
311
+ answered = False
312
+ try:
313
+ async for item in self._stream_events(message, history, attachments, scope):
314
+ answered = answered or item[0] == "text"
315
+ yield item
316
+ except client_guard_blocked() as blocked:
317
+ # The final check stopped an outside-model request: nothing was sent.
318
+ yield ("text", ("\n\n" if answered else "") + blocked.user_message)
319
+ return
320
+ except cost_guard.CostCapReached as cap:
321
+ if task.unattended:
322
+ yield ("text", cost_guard.unattended_message(cap))
323
+ else:
324
+ yield ("text", ("\n\n" if answered else "")
325
+ + cost_guard.stop_message(cap, owner=owner))
326
+ return
327
+ # Restricted-client guard: say plainly what was left out (and that
328
+ # HyperSpeed V2 includes it), on every answer that withheld something.
329
+ from hyperclaw import client_guard
330
+ note = client_guard.reply_note(task)
331
+ if note:
332
+ yield ("text", "\n\n" + note)
333
+ if owner and answered and not task.unattended:
334
+ line = cost_guard.spend_line(task)
335
+ if line:
336
+ yield ("text", "\n\n" + line)
337
+
338
+ async def _stream_events(self, message: str, history: list[dict],
339
+ attachments: list | None = None,
340
+ scope: str = GUEST) -> AsyncIterator[tuple]:
341
+ from hyperclaw.providers import registry, stream_with_failover
342
+
343
+ messages = self._prepare_messages(message, history, attachments)
344
+ kinds = {b.get("type") for b in (attachments or []) if isinstance(b, dict)}
345
+ if kinds & {"document", "image"}:
346
+ # Native file blocks need a provider that reads them. Text-only
347
+ # attachments (inlined files, locally extracted PDF text) ride
348
+ # the primary slot like any chat turn — HyperSpeed V2 included.
349
+ need = {"chat"} | ({"documents"} if "document" in kinds else set()) \
350
+ | ({"images"} if "image" in kinds else set())
351
+ slot = "vision"
352
+ else:
353
+ slot, need = "primary", {"chat", "streaming"}
354
+ # /model on Telegram (docs/MODEL-SWITCHING.md): the chosen model first.
355
+ from hyperclaw.model_catalog import TELEGRAM, prefer_selected
356
+ reg = registry()
357
+ candidates = prefer_selected(reg, TELEGRAM, reg.resolve(slot, need), need)
358
+ # No automatic fallback to a third-party provider without the owner's
359
+ # consent (model_policy); the allowed list already filtered resolve().
360
+ from hyperclaw.model_policy import fallback_ladder
361
+ candidates = fallback_ladder(candidates)
362
+ if not candidates:
363
+ yield ("text", f"[Error: no configured provider supports this turn ({slot})]")
364
+ return
365
+
366
+ async def attempt(prov, model):
367
+ async for item in self._attempt_stream(prov, model, messages, scope=scope):
368
+ yield item
369
+
370
+ async for item in stream_with_failover(candidates, attempt):
371
+ yield item
372
+
373
+ async def stream_chat(self, message: str, history: list[dict],
374
+ scope: str = GUEST) -> AsyncIterator[str]:
375
+ """Stream a response token by token."""
376
+ messages = self._prepare_messages(message, history)
377
+ system_prompt = self._load_system_prompt(scope=scope)
378
+ try:
379
+ if _USE_OPENAI_COMPAT:
380
+ stream = await self._oai.chat.completions.create(
381
+ model=MODEL,
382
+ max_tokens=chat_max_tokens("openai", MODEL),
383
+ messages=[{"role": "system", "content": system_prompt}] + messages,
384
+ stream=True,
385
+ )
386
+ async for chunk in stream:
387
+ delta = chunk.choices[0].delta.content
388
+ if delta:
389
+ yield delta
390
+ else:
391
+ async_client = guard_client(anthropic.AsyncAnthropic(
392
+ api_key=os.environ.get("ANTHROPIC_API_KEY", "")), "anthropic")
393
+ async with async_client.messages.stream(
394
+ model=MODEL,
395
+ max_tokens=chat_max_tokens("anthropic", MODEL),
396
+ system=system_prompt,
397
+ messages=messages,
398
+ ) as stream:
399
+ async for text in stream.text_stream:
400
+ yield text
401
+ except Exception as e:
402
+ yield f"[Error: {e}]"
403
+
404
+ def _prepare_messages(self, message: str, history: list[dict],
405
+ attachments: list | None = None) -> list[dict]:
406
+ """Prepare messages for the API call, trimming to MAX_HISTORY.
407
+
408
+ attachments: optional Anthropic content blocks (document / image)
409
+ that ride along with the user's text — how Telegram file uploads
410
+ reach the model."""
411
+ recent = history[-MAX_HISTORY:] if len(history) > MAX_HISTORY else history
412
+ if attachments:
413
+ content = list(attachments) + [{"type": "text", "text": message or "(see attached)"}]
414
+ return list(recent) + [{"role": "user", "content": content}]
415
+ return list(recent) + [{"role": "user", "content": message}]
416
+
417
+ def reload_context(self) -> None:
418
+ """Reload the system prompt (hot-reload without restart)."""
419
+ self.ai_name = _get_ai_name()
420
+ self.system_prompt = self._load_system_prompt()
421
+
422
+
423
+ # Singleton instance
424
+ _agent: Optional[ChatAgent] = None
425
+
426
+
427
+ def get_chat_agent() -> ChatAgent:
428
+ """Get or create the chat agent singleton."""
429
+ global _agent
430
+ if _agent is None:
431
+ _agent = ChatAgent()
432
+ return _agent
433
+
434
+
435
+ # Backwards compatibility aliases
436
+ Solomon = ChatAgent
437
+ get_solomon = get_chat_agent
438
+
439
+
440
+ def client_guard_blocked():
441
+ from hyperclaw.client_guard import RestrictedContentBlocked
442
+ return RestrictedContentBlocked