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,225 @@
1
+ """Thinking / effort / max_tokens parameters for Anthropic Messages requests.
2
+
3
+ One place decides how HyperClaw asks a Claude model to think. Call sites
4
+ never write ``thinking=`` or ``budget_tokens`` themselves; they call
5
+ :func:`thinking_params` with the model id and splat the result into
6
+ ``client.messages.create`` / ``client.messages.stream``.
7
+
8
+ Why: models differ, and sending the wrong shape is a hard 400. The 5.x
9
+ generation (Claude Opus 5.5, Claude Sonnet 5.5, Fable 5/5.1, Opus 5,
10
+ Sonnet 5, Opus 4.7/4.8) rejects ``thinking: {type: "enabled",
11
+ budget_tokens: N}``; they take ``thinking: {type: "adaptive"}`` with depth
12
+ set by ``output_config.effort``. Older models (Haiku 4.5, Sonnet/Opus 4.5
13
+ and earlier) only think with ``enabled`` + ``budget_tokens`` (budget >=
14
+ 1024 and < ``max_tokens``). Models without thinking get no thinking param.
15
+
16
+ The capability table below is data. Per-model overrides live in
17
+ ``models.yaml`` under ``models.<model id>``::
18
+
19
+ models:
20
+ claude-sonnet-5-5:
21
+ thinking: adaptive # adaptive | enabled | none
22
+ effort: high # low | medium | high | xhigh | max
23
+ thinking_budget: 8000 # only used with thinking: enabled
24
+
25
+ An override can never ask a model for a mode it rejects: ``enabled`` on a
26
+ model without ``budget_tokens`` support falls back to ``adaptive``, and an
27
+ effort level the model does not list is ignored (both logged).
28
+
29
+ Unknown models (not in the table, no override): thinking and effort are
30
+ OMITTED. Omitting ``thinking`` is accepted by every Claude model — current
31
+ models then think adaptively by default, older ones answer without
32
+ thinking — so an unknown id can never 400 on these parameters.
33
+
34
+ Model ids are normalised before lookup, so Bedrock (``anthropic.…``,
35
+ ``us.anthropic.…``, ``-v1:0``), Vertex (``@2025…``), dated snapshots
36
+ (``-20251001``) and ``[1m]`` suffixes all resolve to the same entry.
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import logging
42
+ import re
43
+ from dataclasses import dataclass
44
+ from typing import Optional
45
+
46
+ log = logging.getLogger("hyperclaw.anthropic_params")
47
+
48
+ THINKING_MODES = ("adaptive", "enabled", "none")
49
+ ALL_EFFORTS = ("low", "medium", "high", "xhigh", "max")
50
+ _E5 = ALL_EFFORTS
51
+ _E46 = ("low", "medium", "high", "max")
52
+
53
+ DEFAULT_THINKING_BUDGET = 10000
54
+ MIN_THINKING_BUDGET = 1024 # API minimum for budget_tokens
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class ModelThinking:
59
+ mode: str # adaptive | enabled | none
60
+ effort: Optional[str] = None # default output_config.effort (None = omit)
61
+ efforts: tuple = () # effort levels the model accepts
62
+ budget_tokens: bool = False # accepts thinking.type "enabled" + budget
63
+ display: Optional[str] = None # thinking.display to request (adaptive only)
64
+
65
+
66
+ def _adaptive(effort=None, efforts=_E5, display="summarized"):
67
+ return ModelThinking("adaptive", effort, efforts, False, display)
68
+
69
+
70
+ _LEGACY = ModelThinking("enabled", None, (), True, None)
71
+ _NONE = ModelThinking("none", None, (), False, None)
72
+
73
+ # Capability table. Keys are normalised ids (see normalize_model_id).
74
+ # display="summarized" is requested where the API default is "omitted"
75
+ # (empty thinking text), so the TUI's visible-thinking stream has content.
76
+ CAPABILITIES: dict = {
77
+ # 5.5 generation: adaptive + explicit effort. Opus 5.5's API default is
78
+ # "medium" (one below Opus 5), so it is set explicitly; Sonnet 5.5 levels
79
+ # are recalibrated and "medium" is the documented starting point for
80
+ # agentic, multistep tool use.
81
+ "claude-opus-5-5": _adaptive(effort="high"),
82
+ "claude-sonnet-5-5": _adaptive(effort="medium"),
83
+ # Thinking always on; enabled/disabled are 400s.
84
+ "claude-fable-5-1": _adaptive(),
85
+ "claude-mythos-5-1": _adaptive(),
86
+ "claude-fable-5": _adaptive(),
87
+ "claude-mythos-5": _adaptive(),
88
+ # Adaptive only; budget_tokens is a 400.
89
+ "claude-opus-5": _adaptive(),
90
+ "claude-sonnet-5": _adaptive(),
91
+ "claude-opus-4-8": _adaptive(),
92
+ "claude-opus-4-7": _adaptive(),
93
+ # 4.6: adaptive recommended (budget_tokens deprecated); no xhigh; the
94
+ # API already returns summarized thinking, so no display override.
95
+ "claude-opus-4-6": _adaptive(efforts=_E46, display=None),
96
+ "claude-sonnet-4-6": _adaptive(efforts=_E46, display=None),
97
+ # Legacy extended thinking: enabled + budget_tokens only.
98
+ "claude-haiku-4-5": _LEGACY,
99
+ "claude-sonnet-4-5": _LEGACY,
100
+ "claude-opus-4-5": _LEGACY,
101
+ "claude-opus-4-1": _LEGACY,
102
+ "claude-opus-4": _LEGACY,
103
+ "claude-sonnet-4": _LEGACY,
104
+ "claude-3-7-sonnet": _LEGACY,
105
+ # No extended thinking.
106
+ "claude-3-5-haiku": _NONE,
107
+ "claude-3-5-sonnet": _NONE,
108
+ "claude-3-haiku": _NONE,
109
+ "claude-3-opus": _NONE,
110
+ }
111
+
112
+ UNKNOWN = _NONE # see module docstring: omit thinking for unknown ids
113
+
114
+ _PREFIX = re.compile(r"^(?:[a-z]+\.)?anthropic\.") # Bedrock (+ geo)
115
+ _SUFFIXES = (
116
+ re.compile(r"\[[^\]]*\]$"), # [1m]
117
+ re.compile(r"@.*$"), # Vertex @20251001
118
+ re.compile(r"-v\d+(?::\d+)?$"), # Bedrock -v1:0
119
+ re.compile(r"-\d{8}$"), # dated snapshot
120
+ re.compile(r"-latest$"),
121
+ )
122
+
123
+
124
+ def normalize_model_id(model: str) -> str:
125
+ m = (model or "").strip().lower()
126
+ m = m.rsplit("/", 1)[-1] # ARNs / inference-profile paths
127
+ m = _PREFIX.sub("", m)
128
+ for _ in range(2):
129
+ for rx in _SUFFIXES:
130
+ m = rx.sub("", m)
131
+ return m
132
+
133
+
134
+ def capability_for(model: str) -> ModelThinking:
135
+ return CAPABILITIES.get(normalize_model_id(model), UNKNOWN)
136
+
137
+
138
+ def load_model_overrides(model: str) -> dict:
139
+ """``models.<id>`` thinking/effort/thinking_budget from models.yaml
140
+ (user copy, else packaged). Matches the raw id first, then the
141
+ normalised one. Never raises."""
142
+ try:
143
+ import yaml
144
+ from hyperclaw.api_utils import find_config
145
+ p = find_config("models.yaml")
146
+ data = yaml.safe_load(p.read_text()) if p.exists() else {}
147
+ models = (data or {}).get("models") or {}
148
+ entry = models.get(model)
149
+ if entry is None:
150
+ norm = normalize_model_id(model)
151
+ entry = next((v for k, v in models.items()
152
+ if normalize_model_id(str(k)) == norm), None)
153
+ entry = entry or {}
154
+ return {k: entry[k] for k in ("thinking", "effort", "thinking_budget")
155
+ if k in entry and entry[k] is not None}
156
+ except Exception as e: # config problems must not break a chat turn
157
+ log.warning(f"models.yaml thinking overrides unreadable ({e})")
158
+ return {}
159
+
160
+
161
+ def thinking_params(model: str, max_tokens: int = 16000,
162
+ overrides: Optional[dict] = None,
163
+ use_config: bool = True) -> dict:
164
+ """Request kwargs for ``model``: always ``max_tokens``; ``thinking`` and
165
+ ``output_config`` only when the model takes them.
166
+
167
+ ``overrides`` (thinking/effort/thinking_budget) wins over models.yaml,
168
+ which wins over the capability table."""
169
+ cap = capability_for(model)
170
+ ov = {}
171
+ if use_config:
172
+ ov.update(load_model_overrides(model))
173
+ ov.update(overrides or {})
174
+
175
+ mode = str(ov.get("thinking", cap.mode)).lower()
176
+ if mode in ("false", "off", "disabled"):
177
+ mode = "none"
178
+ if mode not in THINKING_MODES:
179
+ log.warning(f"thinking={mode!r} for {model} is not one of {THINKING_MODES}; "
180
+ f"using {cap.mode}")
181
+ mode = cap.mode
182
+ if mode == "enabled" and not cap.budget_tokens:
183
+ log.warning(f"{model} does not accept thinking.type 'enabled'; using adaptive")
184
+ mode = "adaptive"
185
+ if mode == "adaptive" and cap is UNKNOWN and "thinking" not in ov:
186
+ mode = "none"
187
+
188
+ out: dict = {"max_tokens": int(max_tokens)}
189
+ if mode == "adaptive":
190
+ thinking = {"type": "adaptive"}
191
+ if cap.display:
192
+ thinking["display"] = cap.display
193
+ out["thinking"] = thinking
194
+ effort = ov.get("effort", cap.effort)
195
+ if effort is not None:
196
+ effort = str(effort).lower()
197
+ allowed = cap.efforts or ALL_EFFORTS
198
+ if effort in allowed:
199
+ out["output_config"] = {"effort": effort}
200
+ else:
201
+ log.warning(f"effort={effort!r} not supported by {model}; omitted")
202
+ elif mode == "enabled":
203
+ budget = int(ov.get("thinking_budget", DEFAULT_THINKING_BUDGET))
204
+ budget = min(budget, int(max_tokens) - MIN_THINKING_BUDGET) # leave room to answer
205
+ if budget >= MIN_THINKING_BUDGET:
206
+ out["thinking"] = {"type": "enabled", "budget_tokens": budget}
207
+ return out
208
+
209
+
210
+ def as_sdk_kwargs(params: dict, method) -> dict:
211
+ """Adapt :func:`thinking_params` output to the installed SDK: an SDK too
212
+ old to know ``output_config`` (pyproject allows anthropic>=0.28) gets it
213
+ via ``extra_body`` so the wire request is identical."""
214
+ out = dict(params)
215
+ if "output_config" in out:
216
+ try:
217
+ import inspect
218
+ known = "output_config" in inspect.signature(method).parameters
219
+ except (TypeError, ValueError):
220
+ known = True
221
+ if not known:
222
+ extra = dict(out.pop("extra_body", None) or {})
223
+ extra["output_config"] = out.pop("output_config")
224
+ out["extra_body"] = extra
225
+ return out
hyperclaw/api_utils.py ADDED
@@ -0,0 +1,85 @@
1
+ """Helpers for reading Claude API responses and locating config files.
2
+
3
+ Adaptive-thinking models (the Claude 5 family, Opus/Sonnet 4.6 and later)
4
+ may return thinking blocks before — or instead of — text blocks, so
5
+ ``response.content[0]`` is not guaranteed to be a TextBlock. Reading
6
+ ``.content[0].text`` raises AttributeError on those responses. Always go
7
+ through :func:`extract_text` / :func:`extract_json` instead.
8
+ """
9
+
10
+ import json
11
+ import os
12
+ import re
13
+ import shutil
14
+ from pathlib import Path
15
+
16
+
17
+ def extract_text(response, default: str = "") -> str:
18
+ """Join all text blocks of a Messages API response, skipping thinking
19
+ and tool_use blocks. Returns ``default`` when no text block exists."""
20
+ blocks = getattr(response, "content", None) or []
21
+ parts = [b.text for b in blocks if getattr(b, "type", "") == "text" and getattr(b, "text", "")]
22
+ return "\n".join(parts).strip() or default
23
+
24
+
25
+ def extract_json(response):
26
+ """extract_text + json.loads, tolerating ```json fences around the body."""
27
+ text = extract_text(response)
28
+ m = re.search(r"```(?:json)?\s*(.*?)```", text, re.S)
29
+ if m:
30
+ text = m.group(1)
31
+ return json.loads(text)
32
+
33
+
34
+ def gmail_token_path() -> Path:
35
+ """The Gmail OAuth token file the Gmail tools read.
36
+
37
+ Checks ``workspace/secrets/`` (the historical location) then ``secrets/``
38
+ (the directory onboarding creates), under ``$HYPERCLAW_ROOT`` and
39
+ ``~/.hyperclaw``. Returns the first that exists, else the historical path.
40
+ """
41
+ roots = [Path(os.environ.get("HYPERCLAW_ROOT", Path.home() / ".hyperclaw")),
42
+ Path.home() / ".hyperclaw"]
43
+ candidates = []
44
+ for root in roots:
45
+ for sub in ("workspace/secrets", "secrets"):
46
+ c = root / sub / "gmail_token.json"
47
+ if c not in candidates:
48
+ candidates.append(c)
49
+ for c in candidates:
50
+ if c.exists():
51
+ return Path(os.path.realpath(c))
52
+ return candidates[0]
53
+
54
+
55
+ def gmail_configured() -> bool:
56
+ """True when the Gmail tools have credentials: a token file, or the
57
+ GMAIL_CLIENT_ID / GMAIL_CLIENT_SECRET / GMAIL_REFRESH_TOKEN env fallback."""
58
+ env = os.environ
59
+ return (gmail_token_path().exists() or bool(env.get("GMAIL_TOKEN"))
60
+ or all(env.get(k) for k in ("GMAIL_CLIENT_ID", "GMAIL_CLIENT_SECRET",
61
+ "GMAIL_REFRESH_TOKEN")))
62
+
63
+
64
+ def find_config(name: str) -> Path:
65
+ """Locate a HyperClaw config file (e.g. ``agents.yaml``).
66
+
67
+ Order: the user's ``$HYPERCLAW_ROOT/config`` (seeded on first access),
68
+ then a development checkout's ``config/`` directory, then the copy
69
+ shipped inside the package. Always returns the user path so edits land
70
+ in one place; callers should still check ``.exists()``.
71
+ """
72
+ root = Path(os.environ.get("HYPERCLAW_ROOT", Path.home() / ".hyperclaw"))
73
+ user = root / "config" / name
74
+ if user.exists():
75
+ return user
76
+ here = Path(__file__).resolve().parent
77
+ for src in (here.parent / "config" / name, here / "default_config" / name):
78
+ if src.exists():
79
+ try:
80
+ user.parent.mkdir(parents=True, exist_ok=True)
81
+ shutil.copy(src, user)
82
+ return user
83
+ except OSError:
84
+ return src
85
+ return user
@@ -0,0 +1,209 @@
1
+ """
2
+ Send / Edit / Cancel in Telegram (inline buttons).
3
+
4
+ Every item in the durable approval queue (tools/approval.py) is shown to the
5
+ owner chat with its exact content and three buttons. Only the owner can press
6
+ them: a button press from any other chat or user is refused and logged.
7
+
8
+ - Send -> the stored payload runs once, only if the hash on the button still
9
+ matches the stored payload.
10
+ - Edit -> the bot asks for the new content; the owner's next message in that
11
+ chat replaces it, and the new version is shown again with new
12
+ buttons (the old buttons stop working).
13
+ - Cancel -> nothing runs.
14
+
15
+ Cards go out with link previews off. Terminal-only actions (shell, desktop
16
+ control) cannot be approved from here.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import asyncio
21
+ import logging
22
+ from typing import Dict, List, Optional, Tuple
23
+
24
+ from hyperclaw.chat_scope import OWNER, owner_ids, scope_for
25
+ from hyperclaw.tools import approval as ap
26
+
27
+ log = logging.getLogger("hyperclaw.telegram.approval")
28
+
29
+ CB_PREFIX = "ap"
30
+ CHUNK = 3900
31
+ OPS = {"s": "send", "e": "edit", "c": "cancel"}
32
+
33
+
34
+ def callback_data(op: str, item: ap.PendingAction) -> str:
35
+ return f"{CB_PREFIX}:{op}:{item.id}:{item.short_hash}" # < 64 bytes
36
+
37
+
38
+ def parse_callback(data: str) -> Optional[Tuple[str, str, str]]:
39
+ parts = str(data or "").split(":")
40
+ if len(parts) != 4 or parts[0] != CB_PREFIX or parts[1] not in OPS:
41
+ return None
42
+ return parts[1], parts[2], parts[3]
43
+
44
+
45
+ def keyboard(item: ap.PendingAction):
46
+ from telegram import InlineKeyboardButton, InlineKeyboardMarkup
47
+ return InlineKeyboardMarkup([[
48
+ InlineKeyboardButton("Send", callback_data=callback_data("s", item)),
49
+ InlineKeyboardButton("Edit", callback_data=callback_data("e", item)),
50
+ InlineKeyboardButton("Cancel", callback_data=callback_data("c", item)),
51
+ ]])
52
+
53
+
54
+ def card_chunks(item: ap.PendingAction) -> List[str]:
55
+ """The full card split into Telegram-sized parts (the exact content is
56
+ never cut; the last part carries the buttons)."""
57
+ text = ap.render(item)
58
+ if item.tool in ap._terminal_only():
59
+ text += "\n(This can only be approved in the HyperAgents terminal: /approvals.)"
60
+ return [text[i:i + CHUNK] for i in range(0, len(text), CHUNK)] or [text]
61
+
62
+
63
+ def actor_for(chat_id, user_id) -> ap.Actor:
64
+ """Owner only when the chat is the owner chat AND the person pressing is
65
+ the owner (in a private chat the user id is the chat id)."""
66
+ chat_ok = scope_for("telegram", chat_id) == OWNER
67
+ user_ok = user_id is not None and (str(user_id) == str(chat_id)
68
+ or str(user_id) in owner_ids("telegram"))
69
+ role = "owner" if (chat_ok and user_ok) else "guest"
70
+ return ap.Actor(role, str(user_id), "telegram")
71
+
72
+
73
+ async def push_pending(bot, queue: Optional[ap.ApprovalQueue] = None) -> int:
74
+ """Show every not-yet-shown pending item to the owner chat(s)."""
75
+ q = queue or ap.get_queue()
76
+ owners = sorted(owner_ids("telegram"))
77
+ if not owners:
78
+ return 0
79
+ sent = 0
80
+ for item in q.pending(unnotified_only=True):
81
+ notes = []
82
+ for chat in owners:
83
+ try:
84
+ await show(bot, chat, item)
85
+ notes.append(f"telegram:{chat}")
86
+ except Exception as e:
87
+ log.warning("could not show approval %s to the owner chat: %s", item.id,
88
+ type(e).__name__)
89
+ if notes:
90
+ q.mark_notified(item.id, ",".join(notes))
91
+ sent += 1
92
+ return sent
93
+
94
+
95
+ async def show(bot, chat_id, item: ap.PendingAction) -> None:
96
+ from hyperclaw.tools.egress import TELEGRAM_NO_PREVIEW
97
+ from telegram import LinkPreviewOptions
98
+ no_preview = LinkPreviewOptions(is_disabled=TELEGRAM_NO_PREVIEW["link_preview_options"]
99
+ ["is_disabled"])
100
+ chunks = card_chunks(item)
101
+ for i, part in enumerate(chunks):
102
+ last = i == len(chunks) - 1
103
+ await bot.send_message(chat_id=int(chat_id), text=part,
104
+ link_preview_options=no_preview,
105
+ reply_markup=keyboard(item) if last else None)
106
+
107
+
108
+ class TelegramApprovals:
109
+ """Button presses and the Edit conversation for one bot process."""
110
+
111
+ def __init__(self, registry=None, queue: Optional[ap.ApprovalQueue] = None):
112
+ self.registry = registry
113
+ self.queue = queue
114
+ self.awaiting_edit: Dict[int, str] = {} # chat_id -> action id
115
+
116
+ @property
117
+ def q(self) -> ap.ApprovalQueue:
118
+ return self.queue or ap.get_queue()
119
+
120
+ async def on_callback(self, update, context) -> None:
121
+ query = update.callback_query
122
+ parsed = parse_callback(getattr(query, "data", ""))
123
+ if parsed is None:
124
+ return
125
+ op, aid, h = parsed
126
+ chat_id = query.message.chat.id if query.message else None
127
+ user_id = query.from_user.id if query.from_user else None
128
+ actor = actor_for(chat_id, user_id)
129
+ if not actor.is_owner:
130
+ log.warning("approval button pressed by a non-owner (chat %s); refused", chat_id)
131
+ # Records the refusal in the append-only events (raises ApprovalError).
132
+ try:
133
+ self.q.cancel(aid, actor)
134
+ except ap.ApprovalError:
135
+ pass
136
+ await query.answer("Only the owner can approve this.", show_alert=True)
137
+ return
138
+ if op == "s":
139
+ await query.answer("Sending…")
140
+ loop = asyncio.get_running_loop()
141
+ env = await loop.run_in_executor(
142
+ None, lambda: ap.send(aid, h, actor, registry=self.registry, queue=self.q))
143
+ await self._close_card(query)
144
+ await query.message.reply_text(_outcome("Sent" if env.ok else "Not sent", env))
145
+ elif op == "c":
146
+ env = ap.cancel(aid, actor, queue=self.q)
147
+ await query.answer("Cancelled" if "Cancelled" in env.message else "Not cancelled")
148
+ await self._close_card(query)
149
+ await query.message.reply_text(env.message)
150
+ else:
151
+ item = self.q.get(aid)
152
+ if item is None or item.status != ap.PENDING or not item.payload_hash.startswith(h):
153
+ await query.answer("This is no longer pending.", show_alert=True)
154
+ return
155
+ if item.tool in ap._terminal_only():
156
+ await query.answer("Edit this in the HyperAgents terminal.", show_alert=True)
157
+ return
158
+ self.awaiting_edit[int(chat_id)] = aid
159
+ fld = ap.EDIT_FIELD.get(item.tool)
160
+ await query.answer()
161
+ await query.message.reply_text(
162
+ f"Send me the new {fld} for [{aid}] (or a JSON object with all the arguments). "
163
+ f"I'll show it again before anything is sent." if fld else
164
+ f"Send me the new arguments for [{aid}] as a JSON object. I'll show it again "
165
+ f"before anything is sent.")
166
+
167
+ async def on_owner_text(self, update, context) -> bool:
168
+ """The owner's reply after Edit. True when the message was consumed."""
169
+ chat_id = update.effective_chat.id
170
+ aid = self.awaiting_edit.get(int(chat_id))
171
+ if not aid:
172
+ return False
173
+ user_id = update.effective_user.id if update.effective_user else None
174
+ actor = actor_for(chat_id, user_id)
175
+ if not actor.is_owner:
176
+ return False
177
+ self.awaiting_edit.pop(int(chat_id), None)
178
+ try:
179
+ new_item = ap.edit(aid, update.message.text, actor, registry=self.registry,
180
+ queue=self.q)
181
+ except ap.ApprovalError as e:
182
+ await update.message.reply_text(f"Edit not applied: {e}")
183
+ return True
184
+ await show(context.bot, chat_id, new_item)
185
+ self.q.mark_notified(new_item.id, f"telegram:{chat_id}")
186
+ return True
187
+
188
+ @staticmethod
189
+ async def _close_card(query) -> None:
190
+ try:
191
+ await query.edit_message_reply_markup(reply_markup=None)
192
+ except Exception:
193
+ pass
194
+
195
+
196
+ def _outcome(prefix: str, env) -> str:
197
+ rid = f" (receipt {env.receipt_id})" if getattr(env, "receipt_id", None) else ""
198
+ return f"{prefix}: {env.status}. {env.message[:3000]}{rid}"
199
+
200
+
201
+ async def pending_loop(bot, interval: float = 5.0) -> None:
202
+ """Background: deliver cards queued by any surface (bridge, sub-agents,
203
+ dashboard) to the owner chat."""
204
+ while True:
205
+ try:
206
+ await push_pending(bot)
207
+ except Exception:
208
+ log.warning("approval push failed", exc_info=True)
209
+ await asyncio.sleep(interval)
@@ -0,0 +1,80 @@
1
+ """
2
+ Send / Edit / Cancel in the HyperAgents terminal.
3
+
4
+ When a tool call in the terminal is queued for approval, the owner sees the
5
+ exact content right away and answers s (send), e (edit) or c (cancel). Edit
6
+ asks for the new content and shows it again. The terminal is the owner's own
7
+ surface, so its answers are owner decisions; nothing here runs without one.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ from typing import Callable, Optional
13
+
14
+ from hyperclaw.tools import approval as ap
15
+ from hyperclaw.tools.envelope import Status, make
16
+
17
+
18
+ def interactive() -> bool:
19
+ try:
20
+ return sys.stdin.isatty() and sys.stdout.isatty()
21
+ except (AttributeError, ValueError):
22
+ return False
23
+
24
+
25
+ def prompt(item: ap.PendingAction, *, input_fn: Optional[Callable[[str], str]] = None,
26
+ print_fn: Optional[Callable[[str], None]] = None, registry=None,
27
+ queue: Optional[ap.ApprovalQueue] = None, ledger=None):
28
+ """Ask the owner about one queued item. Returns the envelope the model
29
+ sees: the tool's real result after Send, or a 'nothing was done' result."""
30
+ import builtins
31
+ input_fn = input_fn or builtins.input
32
+ print_fn = print_fn or builtins.print
33
+ q = queue or ap.get_queue()
34
+ while True:
35
+ try:
36
+ q.mark_notified(item.id, "tui") # shown here; no second card elsewhere
37
+ except Exception:
38
+ pass
39
+ print_fn("\n" + ap.render(item))
40
+ try:
41
+ ans = input_fn("Send, edit or cancel? [s/e/c] ").strip().lower()
42
+ except (EOFError, KeyboardInterrupt):
43
+ ans = "c"
44
+ if ans in ("s", "send"):
45
+ return ap.send(item.id, item.payload_hash, ap.TERMINAL_OWNER, registry=registry,
46
+ queue=q, ledger=ledger)
47
+ if ans in ("c", "cancel", "n", "no"):
48
+ return ap.cancel(item.id, ap.TERMINAL_OWNER, queue=q)
49
+ if ans in ("e", "edit"):
50
+ fld = ap.EDIT_FIELD.get(item.tool)
51
+ hint = (f"New {fld} (or a JSON object of all arguments)" if fld
52
+ else "New arguments as a JSON object")
53
+ try:
54
+ new = input_fn(f"{hint}: ")
55
+ except (EOFError, KeyboardInterrupt):
56
+ continue
57
+ try:
58
+ item = ap.edit(item.id, new, ap.TERMINAL_OWNER, registry=registry, queue=q)
59
+ except ap.ApprovalError as e:
60
+ print_fn(f"Edit not applied: {e}")
61
+ fresh = q.get(item.id)
62
+ if fresh is None or fresh.status != ap.PENDING:
63
+ return make(Status.PERMISSION_DENIED, f"Not sent: {e}")
64
+ continue
65
+ print_fn("Please answer s, e or c.")
66
+
67
+
68
+ def review_pending(*, input_fn=None, print_fn=None, registry=None) -> int:
69
+ """/approvals: walk every pending item (from any surface) with the prompt.
70
+ Terminal-only tools queued elsewhere can only be approved here."""
71
+ import builtins
72
+ print_fn = print_fn or builtins.print
73
+ items = ap.get_queue().pending()
74
+ if not items:
75
+ print_fn("Nothing is waiting for approval.")
76
+ return 0
77
+ for item in items:
78
+ env = prompt(item, input_fn=input_fn, print_fn=print_fn, registry=registry)
79
+ print_fn(f"[{item.id}] {env.status}: {env.message[:300]}")
80
+ return len(items)