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/naming.py ADDED
@@ -0,0 +1,284 @@
1
+ """Naming the assistant: the first step of onboarding (spec §14).
2
+
3
+ The customer either types a name, or asks for a suggestion. A suggestion is
4
+ ONE name plus a one-sentence reason, generated by the configured model when
5
+ one is reachable, otherwise taken from a fixed starter list (deterministic
6
+ order). The customer keeps it, asks for another, or types their own. The
7
+ chosen name is stored in the customer's own config.json.
8
+
9
+ Everything here is chat-only: no tools, any model, or no model at all.
10
+ :class:`NamingFlow` is a plain state machine, so the command-line
11
+ onboarding, the terminal first run and the Telegram first run share the
12
+ same steps and wording.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ import re
18
+ from typing import Callable, Optional
19
+
20
+ log = logging.getLogger("hyperclaw.naming")
21
+
22
+ # Starter list used when no model is reachable. Written fresh for HyperClaw.
23
+ FALLBACK_NAMES = [
24
+ ("Juniper", "It's evergreen and steady, the kind of name that stays useful in every season."),
25
+ ("Wren", "Wrens are small, quick and attentive, which is how a good assistant should feel."),
26
+ ("Harbor", "A harbor is where things come in and get sorted out safely."),
27
+ ("Quill", "It's a nod to all the notes, drafts and messages we'll write together."),
28
+ ("Linden", "Linden trees are the shady spot people gather under: calm, dependable company."),
29
+ ("Beacon", "It keeps watch and points you to what needs your attention."),
30
+ ("Indigo", "A deep, calm color for an assistant that stays unhurried when you're busy."),
31
+ ("Compass", "It's there to help you find your way through a crowded day."),
32
+ ("Sable", "It's short, distinctive and easy to say in a hurry."),
33
+ ("Marlo", "It's friendly and a little uncommon, so it's clearly yours."),
34
+ ]
35
+
36
+ SUGGEST_WORDS = {"", "suggest", "suggest one", "you pick", "pick one", "surprise me",
37
+ "you choose", "choose", "s", "?"}
38
+ KEEP_WORDS = {"", "y", "yes", "keep", "keep it", "ok", "okay", "sure", "sounds good",
39
+ "love it", "perfect", "k"}
40
+ ANOTHER_WORDS = {"a", "another", "another one", "next", "no", "n", "different",
41
+ "something else", "try again", "again"}
42
+
43
+ MAX_NAME = 32
44
+ _NAME_OK = re.compile(r"^[A-Za-z0-9][A-Za-z0-9 .'\-]*$")
45
+
46
+ # ── exact wording (shared by every surface) ─────────────────────────────
47
+ INTRO = ("First, let's give me a name. Type a name you like, or say \"suggest\" "
48
+ "and I'll propose one.")
49
+ PROPOSAL = ("How about {name}? {reason}\n"
50
+ "Say \"yes\" to keep it, \"another\" for a different one, or type a name of your own.")
51
+ CONFIRMED = "{name} it is. That's saved in your config, and you can change it any time with /name."
52
+ FALLBACK_NOTE = ("(I picked that from my starter list because no model is connected yet. "
53
+ "Once one is, /name suggest lets me come up with a name myself.)")
54
+ KEEP_OR_NEW = "Keep '{name}', or choose a new name?"
55
+ KEPT = "Keeping the name {name}."
56
+ # Terminal wording for the same question (no buttons): keep (or Enter) / new.
57
+ KEEP_OR_NEW_PROMPT = "Keep '{name}', or choose a new name? (keep / new, Enter = keep)"
58
+ KEEP_ANSWERS = {"", "keep", "k", "keep it", "yes", "y"}
59
+ NEW_ANSWERS = {"new", "n", "choose new", "choose a new name", "change", "new name"}
60
+ INVALID = ("That name won't work ({why}). Type a name of up to {max} letters, numbers or "
61
+ "spaces, or say \"suggest\".")
62
+
63
+
64
+ def validate_name(text: str) -> tuple:
65
+ """(clean_name, "") or ("", reason)."""
66
+ name = re.sub(r"\s+", " ", (text or "").strip()).strip("\"'")
67
+ if not name:
68
+ return "", "it's empty"
69
+ if len(name) > MAX_NAME:
70
+ return "", f"it's longer than {MAX_NAME} characters"
71
+ if len(name.split()) > 3:
72
+ return "", "keep it to three words or fewer"
73
+ if not _NAME_OK.match(name) or not re.search(r"[A-Za-z]", name):
74
+ return "", "use letters, numbers, spaces, apostrophes or hyphens"
75
+ return name, ""
76
+
77
+
78
+ # ── proposals ───────────────────────────────────────────────────────────
79
+
80
+ PROPOSE_PROMPT = (
81
+ "Suggest ONE short, friendly name for a new personal AI assistant, and give a "
82
+ "one-sentence reason for it. Do not use any of these names: {avoid}. "
83
+ "Reply on a single line exactly in this form:\n"
84
+ "NAME: <name> | REASON: <one sentence>"
85
+ )
86
+ _REPLY = re.compile(r"NAME:\s*(.+?)\s*\|\s*REASON:\s*(.+)", re.I | re.S)
87
+
88
+
89
+ def _first_sentence(text: str) -> str:
90
+ t = re.sub(r"\s+", " ", text or "").strip()
91
+ m = re.match(r"(.+?[.!?])(\s|$)", t)
92
+ t = m.group(1) if m else t
93
+ return t[:200]
94
+
95
+
96
+ def parse_model_reply(text: str, avoid: set) -> Optional[tuple]:
97
+ m = _REPLY.search(text or "")
98
+ if not m:
99
+ return None
100
+ name, why = validate_name(m.group(1).strip().strip("*_`"))
101
+ reason = _first_sentence(m.group(2).strip().strip("*_`"))
102
+ if why or not reason or name.lower() in {a.lower() for a in avoid}:
103
+ return None
104
+ if not reason.endswith((".", "!", "?")):
105
+ reason += "."
106
+ return name, reason
107
+
108
+
109
+ def fallback_proposal(avoid: set) -> tuple:
110
+ low = {a.lower() for a in avoid}
111
+ for name, reason in FALLBACK_NAMES:
112
+ if name.lower() not in low:
113
+ return name, reason
114
+ # Every starter name was turned down: cycle deterministically.
115
+ name, reason = FALLBACK_NAMES[len(avoid) % len(FALLBACK_NAMES)]
116
+ return name, reason
117
+
118
+
119
+ def propose_name(avoid: set, model_call: Optional[Callable[[str], Optional[str]]] = None) -> tuple:
120
+ """(name, reason, source) with source "model" or "fallback"."""
121
+ if model_call is not None:
122
+ try:
123
+ reply = model_call(PROPOSE_PROMPT.format(
124
+ avoid=", ".join(sorted(avoid)) or "none"))
125
+ parsed = parse_model_reply(reply or "", avoid)
126
+ if parsed:
127
+ return parsed[0], parsed[1], "model"
128
+ except Exception as e: # unreachable, timeout, bad reply: fall back
129
+ log.info(f"name proposal fell back to the starter list: {e}")
130
+ name, reason = fallback_proposal(avoid)
131
+ return name, reason, "fallback"
132
+
133
+
134
+ def default_model_call(timeout: float = 20.0) -> Optional[Callable[[str], Optional[str]]]:
135
+ """A one-shot, tool-free text call on the configured chat model, or None
136
+ when no model is configured (then the starter list is used)."""
137
+ try:
138
+ from hyperclaw.providers import registry
139
+ cands = registry().resolve("primary", {"chat"})
140
+ except Exception:
141
+ return None
142
+ if not cands:
143
+ return None
144
+ prov, model = cands[0]
145
+
146
+ def call(prompt: str) -> Optional[str]:
147
+ from hyperclaw.cost_guard import guard_client, max_output
148
+ if prov.kind == "anthropic":
149
+ import anthropic
150
+ from hyperclaw.api_utils import extract_text
151
+ client = guard_client(anthropic.Anthropic(api_key=prov.api_key, timeout=timeout,
152
+ max_retries=0), getattr(prov, "name", None))
153
+ resp = client.messages.create(model=model,
154
+ max_tokens=max_output("naming", prov.name, model),
155
+ messages=[{"role": "user", "content": prompt}])
156
+ return extract_text(resp)
157
+ import openai
158
+ client = guard_client(openai.OpenAI(api_key=prov.api_key or "unused",
159
+ base_url=prov.base_url or None, timeout=timeout,
160
+ max_retries=0), getattr(prov, "name", None))
161
+ resp = client.chat.completions.create(
162
+ model=model, max_tokens=max_output("naming", prov.name, model),
163
+ messages=[{"role": "user", "content": prompt}])
164
+ return resp.choices[0].message.content if resp.choices else None
165
+
166
+ return call
167
+
168
+
169
+ # ── storage ─────────────────────────────────────────────────────────────
170
+
171
+ def current_name() -> str:
172
+ from hyperclaw.userconfig import load_config
173
+ return str(load_config().get("ai_name") or "").strip()
174
+
175
+
176
+ def is_named() -> bool:
177
+ return bool(current_name())
178
+
179
+
180
+ def save_name(name: str, source: str) -> None:
181
+ """source: "customer" (typed) or "suggested:model" / "suggested:fallback"."""
182
+ from hyperclaw.userconfig import update_config
183
+ update_config(ai_name=name, ai_name_source=source)
184
+
185
+
186
+ # ── the flow ────────────────────────────────────────────────────────────
187
+
188
+ class NamingFlow:
189
+ """ask -> (typed name | suggestion -> keep / another / typed) -> done."""
190
+
191
+ def __init__(self, model_call: Optional[Callable] = None, save: bool = True):
192
+ self.model_call = model_call
193
+ self.save = save
194
+ self.state = "ask"
195
+ self.proposed: Optional[tuple] = None
196
+ self.offered: list = []
197
+ self.name: str = ""
198
+ self.source: str = ""
199
+
200
+ def start(self) -> str:
201
+ self.state = "ask"
202
+ return INTRO
203
+
204
+ def suggest(self) -> str:
205
+ name, reason, source = propose_name(set(self.offered), self.model_call)
206
+ self.offered.append(name)
207
+ self.proposed = (name, reason, source)
208
+ self.state = "proposed"
209
+ return PROPOSAL.format(name=name, reason=reason)
210
+
211
+ def _finish(self, name: str, source: str) -> str:
212
+ self.name, self.source, self.state = name, source, "done"
213
+ if self.save:
214
+ save_name(name, source)
215
+ msg = CONFIRMED.format(name=name)
216
+ if source == "suggested:fallback":
217
+ msg += "\n" + FALLBACK_NOTE
218
+ return msg
219
+
220
+ def handle(self, text: str) -> str:
221
+ t = (text or "").strip()
222
+ low = t.lower().rstrip(".!")
223
+ if self.state == "ask":
224
+ if low in SUGGEST_WORDS:
225
+ return self.suggest()
226
+ elif self.state == "proposed":
227
+ if low in KEEP_WORDS:
228
+ name, _, source = self.proposed
229
+ return self._finish(name, f"suggested:{source}")
230
+ if low in ANOTHER_WORDS or low in SUGGEST_WORDS:
231
+ return self.suggest()
232
+ elif self.state == "done":
233
+ return CONFIRMED.format(name=self.name)
234
+ name, why = validate_name(t)
235
+ if why:
236
+ return INVALID.format(why=why, max=MAX_NAME)
237
+ return self._finish(name, "customer")
238
+
239
+ @property
240
+ def done(self) -> bool:
241
+ return self.state == "done"
242
+
243
+
244
+ def run_interactive(ask: Callable[[str], str], say: Callable[[str], None],
245
+ model_call: Optional[Callable] = None, save: bool = True) -> str:
246
+ """Drive the flow with a prompt function (terminal / onboarding).
247
+ ask(label) returns the customer's line; EOF/empty handling is the
248
+ caller's. Returns the chosen name."""
249
+ flow = NamingFlow(model_call=model_call, save=save)
250
+ say(flow.start())
251
+ label = "Name (Enter = suggest one)"
252
+ for _ in range(50):
253
+ reply = flow.handle(ask(label))
254
+ say(reply)
255
+ if flow.done:
256
+ return flow.name
257
+ label = ("Keep it? yes / another / or type a name" if flow.state == "proposed"
258
+ else "Name (Enter = suggest one)")
259
+ # Pathological input loop: keep whatever was last proposed.
260
+ if flow.proposed:
261
+ say(flow.handle("yes"))
262
+ return flow.name
263
+
264
+
265
+ def run_command(argv: list, model_call: Optional[Callable] = None) -> str:
266
+ """Non-interactive /name: show, set <name>, suggest (proposal only;
267
+ keep it with `/name <that name>`)."""
268
+ if not argv or argv[0].lower() in ("show",):
269
+ cur = current_name()
270
+ return (f"My name is {cur}." if cur else "I don't have a name yet. "
271
+ "Use /name <name>, or /name suggest.")
272
+ if argv[0].lower() == "suggest":
273
+ name, reason, source = propose_name({current_name()} - {""}, model_call)
274
+ msg = (f"How about {name}? {reason}\nTo keep it, send /name {name} "
275
+ f"(or /name suggest for another).")
276
+ if source == "fallback":
277
+ msg += "\n" + FALLBACK_NOTE
278
+ return msg
279
+ text = " ".join(argv[1:] if argv[0].lower() == "set" else argv)
280
+ name, why = validate_name(text)
281
+ if why:
282
+ return INVALID.format(why=why, max=MAX_NAME)
283
+ save_name(name, "customer")
284
+ return CONFIRMED.format(name=name)
hyperclaw/notices.py ADDED
@@ -0,0 +1,65 @@
1
+ """User-facing failure notices (standard ruled by Vaughn, 2026-10-10, 20.7).
2
+
3
+ Every notice a user can see when something fails must:
4
+ 1. say WHICH action failed ("I couldn't send that message"),
5
+ 2. never imply success (no "sent", "done", "delivered", "completed"),
6
+ 3. never carry the technical error. That goes to the log via log_failure().
7
+
8
+ All user-visible failure wording lives here so Telegram, the scheduler,
9
+ Solomon, the TUI bridge, the terminal and the HTTP chat say it one way.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ import re
15
+
16
+ # action id -> the verb phrase after "I couldn't ..."
17
+ ACTIONS: dict = {
18
+ "answer": "answer that message",
19
+ "send_message": "send that message",
20
+ "read_file": "read that file",
21
+ "write_file": "save that file",
22
+ "get_answer": "get an answer to that message",
23
+ "morning_brief": "finish the morning brief",
24
+ "heartbeat": "finish the heartbeat check",
25
+ "all_hands": "finish the all-hands report",
26
+ "request": "carry out that request",
27
+ "tool": "run that tool",
28
+ }
29
+ GENERIC_ACTION = "complete that request"
30
+ _RETRY = "Please try again in a moment."
31
+
32
+ # Words a failure notice must never contain (it would read as success).
33
+ SUCCESS_WORDS = re.compile(
34
+ r"\b(sent|done|delivered|completed|succeeded|success|successfully|finished)\b", re.I)
35
+
36
+
37
+ def action_phrase(action: str | None = None, what: str | None = None) -> str:
38
+ """The phrase naming the failed action. ``what`` is a free-form phrase
39
+ (e.g. "run the calendar tool") and wins over ``action``; an unknown or
40
+ missing action falls back to a generic phrase that still names a request."""
41
+ if what and what.strip():
42
+ return what.strip()
43
+ return ACTIONS.get(action or "", GENERIC_ACTION)
44
+
45
+
46
+ def failure_notice(action: str | None = None, *, what: str | None = None,
47
+ detail: str = "", retry: bool = True) -> str:
48
+ """'Sorry, I couldn't <action>. <detail> Please try again in a moment.'
49
+
50
+ ``detail`` is a plain-language sentence for the user (never exception text).
51
+ """
52
+ text = f"Sorry, I couldn't {action_phrase(action, what)}."
53
+ if detail:
54
+ text += " " + detail.strip()
55
+ if retry:
56
+ text += " " + _RETRY
57
+ return text
58
+
59
+
60
+ def log_failure(logger: logging.Logger, action: str | None, error=None, *,
61
+ context: str = "", exc_info: bool = False) -> None:
62
+ """Log the technical error for a failed action (the only place it goes)."""
63
+ logger.error("%s failed%s: %s", action or "action",
64
+ f" ({context})" if context else "", str(error)[:500] if error else "no detail",
65
+ exc_info=exc_info)
@@ -0,0 +1,106 @@
1
+ """Split OpenAI-compatible streaming deltas into ("thinking" | "text", str).
2
+
3
+ Reasoning models served through vLLM (HyperSpeed V2 runs the qwen3
4
+ reasoning parser) stream their chain of thought in a side field
5
+ (``delta.reasoning_content``; newer vLLM builds call it ``delta.reasoning``)
6
+ and the answer in ``delta.content``. Without a reasoning parser the same
7
+ model writes ``<think>…</think>`` inline in ``content``. Either way the
8
+ reasoning must never be shown or saved as the answer, and the answer's
9
+ leading blank lines (qwen3 starts content with "\\n\\n") are dropped.
10
+
11
+ Used by every OpenAI-schema streaming loop (solomon, the TUI) so they
12
+ classify chunks the same way.
13
+ """
14
+
15
+ REASONING_FIELDS = ("reasoning_content", "reasoning")
16
+ OPEN_TAG, CLOSE_TAG = "<think>", "</think>"
17
+
18
+
19
+ def _field(delta, name):
20
+ if delta is None:
21
+ return None
22
+ if isinstance(delta, dict):
23
+ return delta.get(name)
24
+ val = getattr(delta, name, None)
25
+ if val is None:
26
+ extra = getattr(delta, "model_extra", None) or {}
27
+ val = extra.get(name)
28
+ return val
29
+
30
+
31
+ def _partial_tag_len(buf: str) -> int:
32
+ """Length of the longest suffix of buf that could start a think tag."""
33
+ for n in range(min(len(buf), len(CLOSE_TAG) - 1), 0, -1):
34
+ tail = buf[-n:]
35
+ if OPEN_TAG.startswith(tail) or CLOSE_TAG.startswith(tail):
36
+ return n
37
+ return 0
38
+
39
+
40
+ class DeltaSplitter:
41
+ """Stateful: feed() each streamed delta, flush() once at the end."""
42
+
43
+ def __init__(self):
44
+ self.in_think = False
45
+ self.text_started = False
46
+ self._pending = ""
47
+ self.text_chars = 0
48
+ self.thinking_chars = 0
49
+
50
+ def _emit(self, out, kind, s):
51
+ if not s:
52
+ return
53
+ if kind == "text":
54
+ if not self.text_started:
55
+ s = s.lstrip()
56
+ if not s:
57
+ return
58
+ self.text_started = True
59
+ self.text_chars += len(s)
60
+ else:
61
+ self.thinking_chars += len(s)
62
+ out.append((kind, s))
63
+
64
+ def feed(self, delta) -> list:
65
+ out = []
66
+ for name in REASONING_FIELDS:
67
+ r = _field(delta, name)
68
+ if isinstance(r, str) and r:
69
+ self._emit(out, "thinking", r)
70
+ break
71
+ content = _field(delta, "content")
72
+ if isinstance(content, str) and content:
73
+ self._feed_content(content, out)
74
+ return out
75
+
76
+ def _feed_content(self, s, out):
77
+ buf = self._pending + s
78
+ self._pending = ""
79
+ while buf:
80
+ if self.in_think:
81
+ i = buf.find(CLOSE_TAG)
82
+ if i >= 0:
83
+ self._emit(out, "thinking", buf[:i])
84
+ buf = buf[i + len(CLOSE_TAG):]
85
+ self.in_think = False
86
+ continue
87
+ else:
88
+ o, c = buf.find(OPEN_TAG), buf.find(CLOSE_TAG)
89
+ hits = [(i, t) for i, t in ((o, OPEN_TAG), (c, CLOSE_TAG)) if i >= 0]
90
+ if hits:
91
+ i, tag = min(hits)
92
+ self._emit(out, "text", buf[:i])
93
+ buf = buf[i + len(tag):]
94
+ self.in_think = tag == OPEN_TAG # a stray </think> is dropped
95
+ continue
96
+ keep = _partial_tag_len(buf)
97
+ self._emit(out, "thinking" if self.in_think else "text", buf[:len(buf) - keep])
98
+ self._pending = buf[len(buf) - keep:]
99
+ break
100
+
101
+ def flush(self) -> list:
102
+ out = []
103
+ if self._pending:
104
+ self._emit(out, "thinking" if self.in_think else "text", self._pending)
105
+ self._pending = ""
106
+ return out