hyperagents 1.2.3rc5.post3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) hide show
  1. agents/__init__.py +31 -0
  2. agents/background_monitor.py +453 -0
  3. agents/learning.py +460 -0
  4. agents/planner.py +421 -0
  5. agents/transcriber.py +341 -0
  6. civilization/__init__.py +51 -0
  7. civilization/api/__init__.py +7 -0
  8. civilization/api/civilization_api.py +361 -0
  9. civilization/graph/__init__.py +19 -0
  10. civilization/graph/knowledge_linker.py +348 -0
  11. civilization/graph/org_graph.py +348 -0
  12. civilization/graph/process_graph.py +386 -0
  13. civilization/ingestion/__init__.py +20 -0
  14. civilization/ingestion/chunker.py +273 -0
  15. civilization/ingestion/document_ingestor.py +358 -0
  16. civilization/ingestion/embedder.py +262 -0
  17. civilization/ingestion/extractor.py +224 -0
  18. civilization/interview/__init__.py +19 -0
  19. civilization/interview/gap_detector.py +380 -0
  20. civilization/interview/interviewer.py +323 -0
  21. civilization/interview/questioner.py +363 -0
  22. civilization/retrieval/__init__.py +17 -0
  23. civilization/retrieval/civilization_rag.py +316 -0
  24. civilization/retrieval/context_injector.py +342 -0
  25. civilization/retrieval/relevance_ranker.py +287 -0
  26. civilization/schema.py +299 -0
  27. civilization/store.py +177 -0
  28. civilization/sync/__init__.py +16 -0
  29. civilization/sync/confluence_sync.py +317 -0
  30. civilization/sync/gdrive_sync.py +262 -0
  31. civilization/sync/notion_sync.py +237 -0
  32. civilization/versioning/__init__.py +19 -0
  33. civilization/versioning/diff_engine.py +351 -0
  34. civilization/versioning/staleness_detector.py +303 -0
  35. civilization/versioning/version_manager.py +293 -0
  36. cli/__init__.py +1 -0
  37. cli/doctor.py +334 -0
  38. cli/hyperclaw.py +1357 -0
  39. cli/onboarding.py +469 -0
  40. core/__init__.py +1 -0
  41. core/connectors/__init__.py +119 -0
  42. core/connectors/infrastructure.py +772 -0
  43. core/connectors/messaging.py +586 -0
  44. core/connectors/productivity.py +764 -0
  45. core/connectors/smarthome.py +528 -0
  46. core/connectors/social.py +730 -0
  47. core/hyperrouter/__init__.py +23 -0
  48. core/hyperrouter/bandit.py +218 -0
  49. core/hyperrouter/fast_loop.py +68 -0
  50. core/hyperrouter/slow_loop.py +143 -0
  51. core/hyperstate/__init__.py +24 -0
  52. core/hyperstate/certifier.py +154 -0
  53. core/hyperstate/schema.py +160 -0
  54. core/hyperstate/state_manager.py +103 -0
  55. core/hyperstate/store.py +163 -0
  56. core/providers/__init__.py +121 -0
  57. core/providers/embeddings.py +389 -0
  58. core/providers/image.py +534 -0
  59. core/providers/llm.py +1935 -0
  60. core/providers/search.py +608 -0
  61. core/providers/voice.py +474 -0
  62. hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
  63. hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
  64. hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
  65. hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
  66. hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
  67. hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
  68. hyperclaw/__init__.py +67 -0
  69. hyperclaw/__main__.py +220 -0
  70. hyperclaw/agent.py +775 -0
  71. hyperclaw/agent_coordinator.py +577 -0
  72. hyperclaw/anthropic_params.py +225 -0
  73. hyperclaw/api_utils.py +85 -0
  74. hyperclaw/approval_telegram.py +209 -0
  75. hyperclaw/approval_terminal.py +80 -0
  76. hyperclaw/attachments.py +177 -0
  77. hyperclaw/branding.py +55 -0
  78. hyperclaw/brief/__init__.py +11 -0
  79. hyperclaw/brief/cli.py +204 -0
  80. hyperclaw/brief/collect.py +146 -0
  81. hyperclaw/brief/compose.py +333 -0
  82. hyperclaw/brief/diff.py +110 -0
  83. hyperclaw/brief/errors.py +99 -0
  84. hyperclaw/brief/google.py +269 -0
  85. hyperclaw/brief/google_auth.py +294 -0
  86. hyperclaw/brief/links.py +124 -0
  87. hyperclaw/brief/service.py +424 -0
  88. hyperclaw/brief/store.py +205 -0
  89. hyperclaw/brief/tasks.py +200 -0
  90. hyperclaw/chat_scope.py +115 -0
  91. hyperclaw/cli_tui.py +497 -0
  92. hyperclaw/client_guard.py +717 -0
  93. hyperclaw/connection_check.py +113 -0
  94. hyperclaw/cost_guard.py +1728 -0
  95. hyperclaw/default_config/agents.yaml +534 -0
  96. hyperclaw/default_config/hyperclaw.yaml +27 -0
  97. hyperclaw/default_config/models.yaml +161 -0
  98. hyperclaw/deployment.py +39 -0
  99. hyperclaw/digest/__init__.py +9 -0
  100. hyperclaw/digest/cli.py +117 -0
  101. hyperclaw/digest/errors.py +100 -0
  102. hyperclaw/digest/gitlab.py +187 -0
  103. hyperclaw/digest/http.py +113 -0
  104. hyperclaw/digest/redact.py +32 -0
  105. hyperclaw/digest/service.py +271 -0
  106. hyperclaw/digest/slack.py +223 -0
  107. hyperclaw/digest/state.py +124 -0
  108. hyperclaw/digest/summarize.py +198 -0
  109. hyperclaw/digest/tokens.py +143 -0
  110. hyperclaw/distribution.py +46 -0
  111. hyperclaw/documents.py +351 -0
  112. hyperclaw/feeds.py +339 -0
  113. hyperclaw/google_workspace/__init__.py +5 -0
  114. hyperclaw/google_workspace/api.py +151 -0
  115. hyperclaw/google_workspace/auth.py +397 -0
  116. hyperclaw/google_workspace/calendar.py +157 -0
  117. hyperclaw/google_workspace/cli.py +72 -0
  118. hyperclaw/google_workspace/drive.py +236 -0
  119. hyperclaw/google_workspace/errors.py +58 -0
  120. hyperclaw/google_workspace/gmail.py +295 -0
  121. hyperclaw/google_workspace/meet.py +122 -0
  122. hyperclaw/google_workspace/tools.py +319 -0
  123. hyperclaw/hyperspeed.py +57 -0
  124. hyperclaw/integrations_layer.py +1427 -0
  125. hyperclaw/live_state.py +188 -0
  126. hyperclaw/managed_config.py +245 -0
  127. hyperclaw/media_hub.py +179 -0
  128. hyperclaw/memory_manager.py +584 -0
  129. hyperclaw/model_catalog.py +551 -0
  130. hyperclaw/model_policy.py +211 -0
  131. hyperclaw/model_router.py +595 -0
  132. hyperclaw/model_selector.py +72 -0
  133. hyperclaw/naming.py +284 -0
  134. hyperclaw/notices.py +65 -0
  135. hyperclaw/oai_stream.py +106 -0
  136. hyperclaw/orchestrator.py +591 -0
  137. hyperclaw/outbox.py +82 -0
  138. hyperclaw/persona.py +367 -0
  139. hyperclaw/prometheus.py +219 -0
  140. hyperclaw/prompt_assembly.py +157 -0
  141. hyperclaw/provider_setup.py +146 -0
  142. hyperclaw/providers.py +340 -0
  143. hyperclaw/qualification.py +341 -0
  144. hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
  145. hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
  146. hyperclaw/reasoning_display.py +35 -0
  147. hyperclaw/scheduler.py +454 -0
  148. hyperclaw/server.py +847 -0
  149. hyperclaw/setup.py +627 -0
  150. hyperclaw/skills.py +717 -0
  151. hyperclaw/solomon.py +442 -0
  152. hyperclaw/swarm.py +533 -0
  153. hyperclaw/taint.py +832 -0
  154. hyperclaw/team/__init__.py +28 -0
  155. hyperclaw/team/dispatcher.py +431 -0
  156. hyperclaw/team/genesis.py +106 -0
  157. hyperclaw/team/lead.py +110 -0
  158. hyperclaw/team/model.py +135 -0
  159. hyperclaw/team/morning.py +238 -0
  160. hyperclaw/team/repos.py +66 -0
  161. hyperclaw/team/roster.py +264 -0
  162. hyperclaw/team/specialist.py +235 -0
  163. hyperclaw/team/status.py +163 -0
  164. hyperclaw/team/watchers.py +112 -0
  165. hyperclaw/teamsetup/__init__.py +17 -0
  166. hyperclaw/teamsetup/checks.py +208 -0
  167. hyperclaw/teamsetup/item.py +391 -0
  168. hyperclaw/teamsetup/oauth_loopback.py +84 -0
  169. hyperclaw/teamsetup/onboard.py +550 -0
  170. hyperclaw/teamsetup/profile.py +82 -0
  171. hyperclaw/teamsetup/secrets.py +313 -0
  172. hyperclaw/teamsetup/selfcheck.py +295 -0
  173. hyperclaw/teamsetup/service.py +495 -0
  174. hyperclaw/teamsetup/slack_oauth.py +442 -0
  175. hyperclaw/teamsetup/tty.py +62 -0
  176. hyperclaw/telegram_bot.py +1106 -0
  177. hyperclaw/telegram_intake.py +126 -0
  178. hyperclaw/telegram_pairing.py +360 -0
  179. hyperclaw/terminal_provider.py +323 -0
  180. hyperclaw/tools/__init__.py +1 -0
  181. hyperclaw/tools/approval.py +949 -0
  182. hyperclaw/tools/args.py +212 -0
  183. hyperclaw/tools/catalog.py +244 -0
  184. hyperclaw/tools/classify.py +266 -0
  185. hyperclaw/tools/effects.py +724 -0
  186. hyperclaw/tools/egress.py +210 -0
  187. hyperclaw/tools/envelope.py +188 -0
  188. hyperclaw/tools/guard.py +367 -0
  189. hyperclaw/tools/ledger.py +339 -0
  190. hyperclaw/tools/receipts.py +70 -0
  191. hyperclaw/tools/registry.py +318 -0
  192. hyperclaw/tools/service_guard.py +151 -0
  193. hyperclaw/tools/surfaces.py +54 -0
  194. hyperclaw/tools/workspace.py +87 -0
  195. hyperclaw/trading/__init__.py +77 -0
  196. hyperclaw/truth_guard.py +791 -0
  197. hyperclaw/tts.py +124 -0
  198. hyperclaw/tui.py +4255 -0
  199. hyperclaw/tui_bridge.py +938 -0
  200. hyperclaw/unattended.py +60 -0
  201. hyperclaw/updates/__init__.py +54 -0
  202. hyperclaw/updates/check.py +131 -0
  203. hyperclaw/updates/index.py +178 -0
  204. hyperclaw/updates/install.py +110 -0
  205. hyperclaw/updates/managed.py +161 -0
  206. hyperclaw/updates/manifest.py +138 -0
  207. hyperclaw/updates/notices.py +86 -0
  208. hyperclaw/updates/paths.py +53 -0
  209. hyperclaw/updates/release.py +146 -0
  210. hyperclaw/updates/upgrade.py +253 -0
  211. hyperclaw/updates/versioning.py +104 -0
  212. hyperclaw/userconfig.py +119 -0
  213. hyperclaw/xai.py +83 -0
  214. integrations/__init__.py +31 -0
  215. integrations/automation/__init__.py +1 -0
  216. integrations/automation/make.py +87 -0
  217. integrations/automation/zapier.py +87 -0
  218. integrations/base.py +476 -0
  219. integrations/communication/__init__.py +1 -0
  220. integrations/communication/twilio.py +279 -0
  221. integrations/communication/zoom.py +290 -0
  222. integrations/data/__init__.py +1 -0
  223. integrations/data/google_analytics.py +135 -0
  224. integrations/data/postgres.py +182 -0
  225. integrations/data/supabase_connector.py +202 -0
  226. integrations/data/weather_feeds.py +450 -0
  227. integrations/developer/__init__.py +9 -0
  228. integrations/developer/github.py +406 -0
  229. integrations/developer/gitlab.py +298 -0
  230. integrations/enterprise/__init__.py +13 -0
  231. integrations/enterprise/confluence.py +266 -0
  232. integrations/enterprise/hubspot.py +293 -0
  233. integrations/enterprise/jira.py +314 -0
  234. integrations/enterprise/salesforce.py +286 -0
  235. integrations/finance/__init__.py +1 -0
  236. integrations/finance/polymarket_client.py +247 -0
  237. integrations/finance/quickbooks.py +281 -0
  238. integrations/finance/stripe.py +261 -0
  239. integrations/gateway.py +238 -0
  240. integrations/messaging/__init__.py +32 -0
  241. integrations/messaging/discord.py +159 -0
  242. integrations/messaging/email.py +401 -0
  243. integrations/messaging/imessage.py +443 -0
  244. integrations/messaging/signal.py +175 -0
  245. integrations/messaging/slack.py +217 -0
  246. integrations/messaging/sms.py +175 -0
  247. integrations/messaging/teams.py +208 -0
  248. integrations/messaging/telegram.py +260 -0
  249. integrations/messaging/whatsapp.py +203 -0
  250. integrations/productivity/__init__.py +17 -0
  251. integrations/productivity/airtable.py +217 -0
  252. integrations/productivity/asana.py +248 -0
  253. integrations/productivity/google/__init__.py +19 -0
  254. integrations/productivity/google/base.py +89 -0
  255. integrations/productivity/google/calendar.py +272 -0
  256. integrations/productivity/google/docs.py +202 -0
  257. integrations/productivity/google/drive.py +268 -0
  258. integrations/productivity/google/gmail.py +279 -0
  259. integrations/productivity/google/meet.py +218 -0
  260. integrations/productivity/google/sheets.py +222 -0
  261. integrations/productivity/google/tasks.py +199 -0
  262. integrations/productivity/linear.py +279 -0
  263. integrations/productivity/microsoft/__init__.py +13 -0
  264. integrations/productivity/microsoft/base.py +93 -0
  265. integrations/productivity/microsoft/calendar.py +207 -0
  266. integrations/productivity/microsoft/onedrive.py +222 -0
  267. integrations/productivity/microsoft/outlook.py +241 -0
  268. integrations/productivity/microsoft/sharepoint.py +211 -0
  269. integrations/productivity/notion.py +243 -0
  270. integrations/productivity/todoist.py +250 -0
  271. integrations/productivity/trello.py +244 -0
  272. integrations/storage/__init__.py +1 -0
  273. integrations/storage/box.py +279 -0
  274. integrations/storage/dropbox.py +215 -0
  275. integrations/telegram_inbound.py +179 -0
  276. integrations/telegram_poller.py +151 -0
  277. memory/__init__.py +1 -0
  278. memory/causal_graph.py +125 -0
  279. memory/impact_tracker.py +88 -0
  280. memory/vector_memory.py +436 -0
  281. models/__init__.py +12 -0
  282. models/chatjimmy_client.py +210 -0
  283. models/claude_client.py +235 -0
  284. models/claude_code_subagent.py +192 -0
  285. models/openai_client.py +120 -0
  286. models/router.py +162 -0
  287. recursive/discovery_loop.py +118 -0
  288. recursive/optimization_loop.py +135 -0
  289. recursive/validation_loop.py +38 -0
  290. security/__init__.py +1 -0
  291. security/audit_logger.py +106 -0
  292. security/filesystem_guard.py +100 -0
  293. security/hypershield.py +99 -0
  294. security/network_guard.py +97 -0
  295. security/policy_engine.py +198 -0
  296. swarm/__init__.py +1 -0
  297. swarm/agents/__init__.py +0 -0
  298. swarm/agents/base.py +230 -0
  299. swarm/agents/business/__init__.py +0 -0
  300. swarm/agents/business/counsel.py +31 -0
  301. swarm/agents/business/herald.py +32 -0
  302. swarm/agents/business/ledger.py +26 -0
  303. swarm/agents/business/nexus.py +30 -0
  304. swarm/agents/business/ops.py +30 -0
  305. swarm/agents/business/pipeline.py +28 -0
  306. swarm/agents/business/revenue.py +30 -0
  307. swarm/agents/business/sovereign.py +31 -0
  308. swarm/agents/business/strategos.py +27 -0
  309. swarm/agents/business/talent.py +27 -0
  310. swarm/agents/business/venture.py +32 -0
  311. swarm/agents/comms/__init__.py +1 -0
  312. swarm/agents/comms/cipher.py +32 -0
  313. swarm/agents/comms/echo_comms.py +31 -0
  314. swarm/agents/comms/envoy.py +31 -0
  315. swarm/agents/comms/herald_scheduler.py +33 -0
  316. swarm/agents/comms/pulse.py +29 -0
  317. swarm/agents/creative/__init__.py +0 -0
  318. swarm/agents/creative/author.py +27 -0
  319. swarm/agents/creative/lens.py +25 -0
  320. swarm/agents/executive/__init__.py +1 -0
  321. swarm/agents/intelligence/__init__.py +1 -0
  322. swarm/agents/intelligence/arbiter.py +30 -0
  323. swarm/agents/intelligence/sentinel.py +30 -0
  324. swarm/agents/media/__init__.py +0 -0
  325. swarm/agents/personal/__init__.py +0 -0
  326. swarm/agents/personal/atlas.py +27 -0
  327. swarm/agents/personal/hearth.py +26 -0
  328. swarm/agents/personal/midas.py +27 -0
  329. swarm/agents/personal/navigator.py +27 -0
  330. swarm/agents/personal/nourish.py +31 -0
  331. swarm/agents/personal/vitals.py +33 -0
  332. swarm/agents/recursive/__init__.py +0 -0
  333. swarm/agents/recursive/alchemist.py +73 -0
  334. swarm/agents/recursive/calibrator.py +45 -0
  335. swarm/agents/recursive/scout.py +97 -0
  336. swarm/agents/scientific/__init__.py +0 -0
  337. swarm/agents/scientific/cosmos.py +36 -0
  338. swarm/agents/scientific/gaia.py +26 -0
  339. swarm/agents/scientific/medicus.py +33 -0
  340. swarm/agents/scientific/oracle_agent.py +39 -0
  341. swarm/agents/scientific/scribe.py +39 -0
  342. swarm/agents/talent/__init__.py +0 -0
  343. swarm/agents/talent/deal.py +28 -0
  344. swarm/agents/talent/roster.py +29 -0
  345. swarm/agents/talent/scout.py +29 -0
  346. swarm/agents/talent/stage.py +27 -0
  347. swarm/agents/tech/__init__.py +0 -0
  348. swarm/agents/tech/aegis.py +28 -0
  349. swarm/agents/tech/bridge.py +28 -0
  350. swarm/agents/tech/forge.py +28 -0
  351. swarm/agents/trading/__init__.py +1 -0
  352. swarm/agents/trading/global_prediction_engine.py +611 -0
  353. swarm/agents/trading/polymarket_trader.py +165 -0
  354. swarm/agents/trading/prediction_strategist.py +218 -0
  355. swarm/autogen_bridge.py +68 -0
  356. swarm/bid_protocol.py +196 -0
  357. swarm/dispatcher.py +527 -0
  358. swarm/nexus.py +181 -0
  359. swarm/registry.py +112 -0
  360. telegram_listener.py +403 -0
  361. ui/tui/app.py +214 -0
@@ -0,0 +1,135 @@
1
+ """
2
+ The model a specialist talks to.
3
+
4
+ A ``ModelHandle`` gives the specialist loop one call shape (Anthropic
5
+ Messages: system, messages, tools -> content blocks + stop_reason) whatever
6
+ the provider is. Every call:
7
+
8
+ - goes through a cost-guarded client (``cost_guard.guard_client``), so it is
9
+ reserved against the per-task and daily caps BEFORE it is made and settled
10
+ after; at a cap it raises ``CostCapReached`` and nothing is called;
11
+ - sets an explicit max output (``max_tokens``; HC-037);
12
+ - comes from a provider whose tool use is declared AND qualified (signed
13
+ record). Without one the specialist does not run (``NoToolModel``): the
14
+ failure is reported, never papered over with a chat-only model.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import uuid
20
+ from dataclasses import dataclass
21
+ from types import SimpleNamespace
22
+ from typing import Any, Callable, List, Optional
23
+
24
+ from hyperclaw.team.roster import Specialist
25
+
26
+ MAX_OUTPUT_PATH = "specialist" # cost_guard.DEFAULT_MAX_OUTPUT_PER_PATH (!33)
27
+
28
+
29
+ class NoToolModel(RuntimeError):
30
+ """No provider with qualified tool use is configured for specialists."""
31
+
32
+ def __init__(self) -> None:
33
+ super().__init__("no model with qualified tool use is configured, so the specialist "
34
+ "could not run")
35
+
36
+
37
+ @dataclass
38
+ class ModelHandle:
39
+ client: Any # cost-guarded, Anthropic Messages shape (.messages.create)
40
+ model: str
41
+ provider: str = ""
42
+
43
+ def create(self, *, system: str, messages: list, tools: list) -> Any:
44
+ from hyperclaw.cost_guard import max_output
45
+ extra = {"tools": tools} if tools else {}
46
+ return self.client.messages.create(
47
+ model=self.model, max_tokens=max_output(MAX_OUTPUT_PATH, self.provider, self.model),
48
+ system=system, messages=messages, **extra)
49
+
50
+
51
+ # ───────────────────────── OpenAI-compatible providers ─────────────────────────
52
+
53
+ class _OpenAIMessages:
54
+ """Anthropic-shaped .create() over a cost-guarded chat.completions client."""
55
+
56
+ def __init__(self, client, provider_name: str, base_url: str):
57
+ self._client = client
58
+ self._provider = provider_name
59
+ self._base = base_url
60
+
61
+ def create(self, *, model: str, max_tokens: int, system: str, messages: list,
62
+ tools: Optional[list] = None):
63
+ from hyperclaw.truth_guard import history_to_openai
64
+ from hyperclaw.xai import clean_request
65
+ oai = [{"role": "system", "content": system}] + history_to_openai(messages,
66
+ native_tools=True)
67
+ kw = dict(model=model, messages=oai, max_tokens=int(max_tokens), stream=False)
68
+ if tools:
69
+ kw["tools"] = [{"type": "function", "function": {
70
+ "name": t["name"], "description": t.get("description", ""),
71
+ "parameters": t.get("input_schema", {})}} for t in tools]
72
+ kw["tool_choice"] = "auto"
73
+ kw = clean_request(self._provider, self._base, model, kw)
74
+ res = self._client.chat.completions.create(**kw)
75
+ ch = (getattr(res, "choices", None) or [None])[0]
76
+ msg = getattr(ch, "message", None)
77
+ blocks: List[Any] = []
78
+ text = getattr(msg, "content", None) or ""
79
+ if text:
80
+ blocks.append(SimpleNamespace(type="text", text=text))
81
+ for tc in getattr(msg, "tool_calls", None) or []:
82
+ fn = getattr(tc, "function", None)
83
+ raw = getattr(fn, "arguments", "") or "{}"
84
+ try:
85
+ args = json.loads(raw)
86
+ except ValueError:
87
+ args = raw # the registry answers invalid_args, never {}
88
+ blocks.append(SimpleNamespace(type="tool_use", id=getattr(tc, "id", "") or
89
+ f"call_{uuid.uuid4().hex[:12]}",
90
+ name=getattr(fn, "name", ""), input=args))
91
+ fr = getattr(ch, "finish_reason", None)
92
+ stop = {"tool_calls": "tool_use", "length": "max_tokens"}.get(fr, "end_turn")
93
+ return SimpleNamespace(content=blocks, stop_reason=stop)
94
+
95
+
96
+ class _OpenAIShim:
97
+ def __init__(self, client, provider_name: str, base_url: str):
98
+ self.messages = _OpenAIMessages(client, provider_name, base_url)
99
+
100
+
101
+ # ───────────────────────── resolution ─────────────────────────
102
+
103
+ def _candidates(spec: Specialist) -> list:
104
+ from hyperclaw.providers import registry
105
+ reg = registry()
106
+ out = []
107
+ for slot in (spec.tier, "tools", "primary"):
108
+ for c in reg.resolve(slot, {"chat", "tool_use"}):
109
+ if c not in out:
110
+ out.append(c)
111
+ return out
112
+
113
+
114
+ def default_handle(spec: Specialist) -> ModelHandle:
115
+ """A cost-guarded handle on a qualified tool-use model, preferring the
116
+ specialist's tier. Raises NoToolModel when none is configured."""
117
+ from hyperclaw.cost_guard import guard_client
118
+ cands = _candidates(spec)
119
+ if not cands:
120
+ raise NoToolModel()
121
+ prov, model = cands[0]
122
+ if prov.kind == "anthropic":
123
+ import anthropic
124
+ client = guard_client(anthropic.Anthropic(api_key=prov.api_key,
125
+ base_url=prov.base_url or None,
126
+ timeout=60.0, max_retries=0), prov.name)
127
+ return ModelHandle(client, model, prov.name)
128
+ import openai
129
+ raw = guard_client(openai.OpenAI(api_key=prov.api_key or "unused",
130
+ base_url=prov.base_url or None, timeout=60.0,
131
+ max_retries=0), prov.name)
132
+ return ModelHandle(_OpenAIShim(raw, prov.name, prov.base_url or ""), model, prov.name)
133
+
134
+
135
+ HandleFactory = Callable[[Specialist], ModelHandle]
@@ -0,0 +1,238 @@
1
+ """
2
+ The morning brief's "your day" sections (HC-038), at each person's chosen
3
+ time (``brief.time``), extending the existing daily brief: the same
4
+ scheduled job (one background service) and the same Telegram delivery (one
5
+ Telegram connection, link previews off).
6
+
7
+ Four sections, each from whichever connector is present:
8
+
9
+ Today's calendar CalendarSource
10
+ Email that needs you MailSource
11
+ Slack mentions and DMs SlackSource
12
+ MRs waiting on you ReposSource
13
+
14
+ A missing connector is skipped honestly ("not connected"), a failed read is
15
+ named ("could not read ..."), and an empty one says "nothing". Nothing is
16
+ written by a model here: every line is taken from what the source returned,
17
+ so the brief never claims anything that is not on this install.
18
+
19
+ Integration points (marked ``INTEGRATION``): the Gmail / Calendar / Slack
20
+ readers may be replaced by the connector lane's readers; each source is a
21
+ small class with ``connected()`` and ``read(now)``, and tests use fakes.
22
+ Everything read is connector content (``hyperclaw.taint``): it is delivered
23
+ to the owner and never saved to memory, learning or training data.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import logging
28
+ from dataclasses import dataclass
29
+ from datetime import datetime, timedelta, timezone
30
+ from typing import List, Optional, Sequence
31
+
32
+ log = logging.getLogger("hyperclaw.team.morning")
33
+
34
+ MAX_LINES = 8
35
+ LINE_CHARS = 140
36
+
37
+
38
+ class SourceUnavailable(Exception):
39
+ """A connected source that could not be read (the reason is shown)."""
40
+
41
+
42
+ @dataclass
43
+ class Section:
44
+ title: str
45
+ lines: List[str]
46
+ state: str = "ok" # ok | not_connected | failed
47
+
48
+ def render(self) -> str:
49
+ if self.state == "not_connected":
50
+ return f"{self.title}: not connected."
51
+ if self.state == "failed":
52
+ return f"{self.title}: could not read it ({self.lines[0] if self.lines else 'error'})."
53
+ if not self.lines:
54
+ return f"{self.title}: nothing."
55
+ shown = self.lines[:MAX_LINES]
56
+ more = len(self.lines) - len(shown)
57
+ body = "\n".join(f"- {_clip(l)}" for l in shown)
58
+ return f"{self.title}:\n{body}" + (f"\n- and {more} more" if more > 0 else "")
59
+
60
+
61
+ def _clip(s: str) -> str:
62
+ """One line, no web addresses (link previews and taps stay off)."""
63
+ from hyperclaw.team.watchers import _SPACE, _URL
64
+ t = _SPACE.sub(" ", _URL.sub("[link removed]", str(s or ""))).strip()
65
+ return t if len(t) <= LINE_CHARS else t[:LINE_CHARS - 1].rstrip() + "…"
66
+
67
+
68
+ class Source:
69
+ title = ""
70
+
71
+ def connected(self) -> bool:
72
+ raise NotImplementedError
73
+
74
+ def read(self, now: datetime) -> List[str]:
75
+ raise NotImplementedError
76
+
77
+
78
+ # ───────────────────────── default sources ─────────────────────────
79
+
80
+ def _google_connected() -> bool:
81
+ try:
82
+ from hyperclaw.api_utils import gmail_configured
83
+ return gmail_configured()
84
+ except Exception:
85
+ return False
86
+
87
+
88
+ class CalendarSource(Source):
89
+ """INTEGRATION: Google Calendar via integrations_layer (the connector
90
+ lane's reader replaces ``_events``)."""
91
+ title = "Today's calendar"
92
+
93
+ def connected(self) -> bool:
94
+ return _google_connected()
95
+
96
+ def _events(self) -> list:
97
+ from hyperclaw import integrations_layer as il
98
+ return il.calendar_get_events(days_ahead=1)
99
+
100
+ def read(self, now: datetime) -> List[str]:
101
+ events = self._events()
102
+ if events and isinstance(events[0], dict) and events[0].get("error"):
103
+ raise SourceUnavailable("Google Calendar refused the request")
104
+ day = now.date().isoformat()
105
+ out = []
106
+ for e in events:
107
+ start = str(e.get("start") or "")
108
+ if start[:10] != day:
109
+ continue
110
+ when = start[11:16] if len(start) > 10 else "all day"
111
+ out.append(f"{when} {e.get('summary') or 'Untitled'}")
112
+ return out
113
+
114
+
115
+ class MailSource(Source):
116
+ """INTEGRATION: Gmail via integrations_layer: unread mail in the inbox
117
+ from the last day that Gmail marks important."""
118
+ title = "Email that needs you"
119
+ QUERY = "in:inbox is:unread is:important newer_than:1d"
120
+
121
+ def connected(self) -> bool:
122
+ return _google_connected()
123
+
124
+ def _search(self) -> list:
125
+ from hyperclaw import integrations_layer as il
126
+ return il.gmail_search(self.QUERY, max_results=15)
127
+
128
+ def read(self, now: datetime) -> List[str]:
129
+ mails = self._search()
130
+ if mails and isinstance(mails[0], dict) and mails[0].get("error"):
131
+ raise SourceUnavailable("Gmail refused the request")
132
+ return [f"{m.get('from') or 'someone'}: {m.get('subject') or '(no subject)'}"
133
+ for m in mails if isinstance(m, dict) and not m.get("is_sent")]
134
+
135
+
136
+ class SlackSource(Source):
137
+ """INTEGRATION: the connected digest's read-only Slack collector (the
138
+ owner's user token): mentions and DMs since yesterday morning."""
139
+ title = "Slack mentions and DMs"
140
+
141
+ def connected(self) -> bool:
142
+ try:
143
+ from hyperclaw.digest import state
144
+ return "slack" in state.connections()
145
+ except Exception:
146
+ return False
147
+
148
+ def _collect(self, since: datetime):
149
+ from hyperclaw.digest import service, state
150
+ meta = state.connections().get("slack") or {}
151
+ token = service._read_token("slack", meta)
152
+ if not token:
153
+ raise SourceUnavailable("the Slack token is missing")
154
+ col = service.make_collector("slack", token)
155
+ try:
156
+ col.verify()
157
+ return col.collect(since)
158
+ finally:
159
+ col.close()
160
+
161
+ def read(self, now: datetime) -> List[str]:
162
+ from hyperclaw.digest.errors import DigestError
163
+ try:
164
+ res = self._collect(now - timedelta(hours=24))
165
+ except DigestError as e:
166
+ raise SourceUnavailable(e.text)
167
+ if not res.ok:
168
+ raise SourceUnavailable("Slack could not be read")
169
+ out = []
170
+ for i in res.items:
171
+ if not isinstance(i, dict):
172
+ continue
173
+ where = "DM" if i.get("kind") in ("dm", "group_dm") else i.get("where") or "Slack"
174
+ out.append(f"{i.get('from') or 'someone'} ({where}): {i.get('text') or ''}")
175
+ return out
176
+
177
+
178
+ class ReposSource(Source):
179
+ title = "MRs waiting on you"
180
+
181
+ def connected(self) -> bool:
182
+ try:
183
+ from hyperclaw.digest import state
184
+ return "gitlab" in state.connections()
185
+ except Exception:
186
+ return False
187
+
188
+ def read(self, now: datetime) -> List[str]:
189
+ from hyperclaw.team import repos
190
+ items, problem = repos.waiting(now)
191
+ if problem:
192
+ raise SourceUnavailable(problem.rstrip("."))
193
+ out = []
194
+ for i in items:
195
+ if i.get("kind") == "todo":
196
+ out.append(f"to-do: {i.get('action') or ''} {i.get('title') or ''}".strip())
197
+ else:
198
+ role = "review" if i.get("kind") == "mr_reviewing" else "assigned"
199
+ out.append(f"{i.get('ref') or ''} {i.get('title') or ''} ({role})")
200
+ return out
201
+
202
+
203
+ def default_sources() -> List[Source]:
204
+ return [CalendarSource(), MailSource(), SlackSource(), ReposSource()]
205
+
206
+
207
+ # ───────────────────────── compose ─────────────────────────
208
+
209
+ def sections(now: Optional[datetime] = None,
210
+ sources: Optional[Sequence[Source]] = None) -> List[Section]:
211
+ now = now or datetime.now(timezone.utc)
212
+ out = []
213
+ for src in (default_sources() if sources is None else sources):
214
+ try:
215
+ if not src.connected():
216
+ out.append(Section(src.title, [], "not_connected"))
217
+ continue
218
+ out.append(Section(src.title, list(src.read(now))))
219
+ except SourceUnavailable as e:
220
+ out.append(Section(src.title, [str(e)], "failed"))
221
+ except Exception as e:
222
+ log.warning("morning: %s not read (%s)", src.title, type(e).__name__, exc_info=True)
223
+ out.append(Section(src.title, ["an internal error"], "failed"))
224
+ return out
225
+
226
+
227
+ def render(now: Optional[datetime] = None,
228
+ sources: Optional[Sequence[Source]] = None) -> str:
229
+ return "\n\n".join(s.render() for s in sections(now, sources))
230
+
231
+
232
+ def enabled() -> bool:
233
+ """On unless the person turned it off (config brief.morning = false)."""
234
+ try:
235
+ from hyperclaw.brief import store
236
+ return store.config().get("morning", True) is not False
237
+ except Exception:
238
+ return True
@@ -0,0 +1,66 @@
1
+ """
2
+ Merge requests and to-dos waiting on the person (the Repos specialist's one
3
+ tool, and the morning brief's "MRs waiting on you" section).
4
+
5
+ Read-only, through the connected digest's GitLab connection (read-only token,
6
+ verified scopes; ``hyperclaw.digest.gitlab``). Everything returned is GitLab
7
+ content: marked as connector content (``hyperclaw.taint``).
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from datetime import datetime, timedelta, timezone
13
+ from typing import Callable, Optional
14
+
15
+ NOT_CONNECTED = "GitLab is not connected (hyperclaw digest connect gitlab)."
16
+ WINDOW = timedelta(days=14)
17
+
18
+
19
+ def _collect(now: datetime, *, collector_factory: Optional[Callable] = None):
20
+ from hyperclaw.digest import service, state
21
+ from hyperclaw.digest.errors import DigestError
22
+ meta = state.connections().get("gitlab")
23
+ if not meta:
24
+ return None, NOT_CONNECTED
25
+ col = None
26
+ try:
27
+ if collector_factory is not None:
28
+ col = collector_factory()
29
+ else:
30
+ token = service._read_token("gitlab", meta)
31
+ if not token:
32
+ return None, NOT_CONNECTED
33
+ col = service.make_collector("gitlab", token, gitlab_url=meta.get("url", ""))
34
+ col.verify()
35
+ return col.collect(now - WINDOW), ""
36
+ except DigestError as e:
37
+ return None, f"Could not read GitLab: {e.text}"
38
+ finally:
39
+ if col is not None:
40
+ try:
41
+ col.close()
42
+ except Exception:
43
+ pass
44
+
45
+
46
+ def waiting(now: Optional[datetime] = None, *, collector_factory: Optional[Callable] = None):
47
+ """(items, problem). items: MRs assigned to / awaiting review from the
48
+ person, and their pending to-dos. problem: '' or why nothing was read."""
49
+ now = now or datetime.now(timezone.utc)
50
+ res, problem = _collect(now, collector_factory=collector_factory)
51
+ if res is None:
52
+ return [], problem
53
+ if not res.ok:
54
+ return [], "Could not read GitLab."
55
+ keep = ("mr_assigned", "mr_reviewing", "todo")
56
+ return [i for i in res.items if isinstance(i, dict) and i.get("kind") in keep], ""
57
+
58
+
59
+ def tool_review_queue(args: dict) -> str:
60
+ """The gitlab_review_queue tool: JSON list, or a not-connected / failure line."""
61
+ items, problem = waiting()
62
+ if problem:
63
+ return problem
64
+ out = [{k: i.get(k) for k in ("kind", "ref", "title", "author", "from", "action", "updated",
65
+ "when", "link") if i.get(k)} for i in items]
66
+ return json.dumps({"waiting_on_you": out, "count": len(out)})