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/agent.py ADDED
@@ -0,0 +1,775 @@
1
+ """
2
+ HyperClaw Agent — Assistant with Tool Execution
3
+ Full computer control using Anthropic tool-use API.
4
+ """
5
+
6
+ import os
7
+ import asyncio
8
+ import subprocess
9
+ import glob as globlib
10
+ import re
11
+ from datetime import datetime
12
+ from pathlib import Path
13
+ from typing import AsyncIterator, Optional, Any
14
+
15
+ import anthropic
16
+ from hyperclaw.cost_guard import max_output
17
+ from dotenv import load_dotenv
18
+
19
+ load_dotenv()
20
+
21
+ HYPERCLAW_ROOT = Path(os.environ.get("HYPERCLAW_ROOT", str(Path.home() / ".hyperclaw")))
22
+ WORKSPACE_PATH = HYPERCLAW_ROOT / "workspace"
23
+ MODEL = "claude-sonnet-4-6"
24
+ MAX_TOKENS = 16000
25
+
26
+ # Context files to load
27
+ CONTEXT_FILES = [
28
+ "SOUL.md",
29
+ "IDENTITY.md",
30
+ "USER.md",
31
+ "MEMORY.md",
32
+ "memory/instincts.md",
33
+ "memory/core-episodes.md",
34
+ ]
35
+
36
+ # Tool definitions for Claude API
37
+ TOOLS = [
38
+ {
39
+ "name": "bash",
40
+ "description": "Execute a bash command on the local system. Use for system operations, git, file management, etc.",
41
+ "input_schema": {
42
+ "type": "object",
43
+ "properties": {
44
+ "command": {
45
+ "type": "string",
46
+ "description": "The bash command to execute"
47
+ },
48
+ "timeout": {
49
+ "type": "integer",
50
+ "description": "Timeout in seconds (default 120)",
51
+ "default": 120
52
+ }
53
+ },
54
+ "required": ["command"]
55
+ }
56
+ },
57
+ {
58
+ "name": "read_file",
59
+ "description": "Read the contents of a file. Use absolute paths.",
60
+ "input_schema": {
61
+ "type": "object",
62
+ "properties": {
63
+ "path": {
64
+ "type": "string",
65
+ "description": "Absolute path to the file"
66
+ },
67
+ "limit": {
68
+ "type": "integer",
69
+ "description": "Maximum lines to read (default all)"
70
+ }
71
+ },
72
+ "required": ["path"]
73
+ }
74
+ },
75
+ {
76
+ "name": "write_file",
77
+ "description": "Write content to a file. Creates parent directories if needed.",
78
+ "input_schema": {
79
+ "type": "object",
80
+ "properties": {
81
+ "path": {
82
+ "type": "string",
83
+ "description": "Absolute path to the file"
84
+ },
85
+ "content": {
86
+ "type": "string",
87
+ "description": "Content to write"
88
+ }
89
+ },
90
+ "required": ["path", "content"]
91
+ }
92
+ },
93
+ {
94
+ "name": "edit_file",
95
+ "description": "Edit a file by replacing old_string with new_string.",
96
+ "input_schema": {
97
+ "type": "object",
98
+ "properties": {
99
+ "path": {
100
+ "type": "string",
101
+ "description": "Absolute path to the file"
102
+ },
103
+ "old_string": {
104
+ "type": "string",
105
+ "description": "The text to find and replace"
106
+ },
107
+ "new_string": {
108
+ "type": "string",
109
+ "description": "The replacement text"
110
+ }
111
+ },
112
+ "required": ["path", "old_string", "new_string"]
113
+ }
114
+ },
115
+ {
116
+ "name": "glob",
117
+ "description": "Find files matching a glob pattern.",
118
+ "input_schema": {
119
+ "type": "object",
120
+ "properties": {
121
+ "pattern": {
122
+ "type": "string",
123
+ "description": "Glob pattern (e.g., '**/*.py', '*.md')"
124
+ },
125
+ "path": {
126
+ "type": "string",
127
+ "description": "Base directory (default: home)",
128
+ "default": str(Path.home())
129
+ }
130
+ },
131
+ "required": ["pattern"]
132
+ }
133
+ },
134
+ {
135
+ "name": "grep",
136
+ "description": "Search for a pattern in files using ripgrep.",
137
+ "input_schema": {
138
+ "type": "object",
139
+ "properties": {
140
+ "pattern": {
141
+ "type": "string",
142
+ "description": "Regex pattern to search for"
143
+ },
144
+ "path": {
145
+ "type": "string",
146
+ "description": "Directory or file to search in",
147
+ "default": str(Path.home())
148
+ },
149
+ "file_pattern": {
150
+ "type": "string",
151
+ "description": "File glob to filter (e.g., '*.py')"
152
+ }
153
+ },
154
+ "required": ["pattern"]
155
+ }
156
+ },
157
+ {
158
+ "name": "list_directory",
159
+ "description": "List contents of a directory.",
160
+ "input_schema": {
161
+ "type": "object",
162
+ "properties": {
163
+ "path": {
164
+ "type": "string",
165
+ "description": "Directory path to list"
166
+ }
167
+ },
168
+ "required": ["path"]
169
+ }
170
+ }
171
+ ]
172
+
173
+
174
+ def execute_tool(name: str, inputs: dict) -> str:
175
+ """Execute a tool and return the result."""
176
+ if inputs is None:
177
+ inputs = {}
178
+ try:
179
+ if name == "bash":
180
+ cmd = inputs.get("command")
181
+ if not cmd:
182
+ return "Error: bash requires 'command' parameter"
183
+ # Service recovery (bench 17.4/17.5): this loop has no approval
184
+ # channel, so no service is ever started, stopped or killed here.
185
+ from hyperclaw.tools.service_guard import check_command
186
+ svc = check_command(cmd)
187
+ if svc is not None:
188
+ return (f"Error: not run. Service and process control needs the owner's "
189
+ f"approval in the HyperAgents terminal ({svc.reason}).")
190
+ return _exec_bash(cmd, inputs.get("timeout", 120))
191
+ elif name == "read_file":
192
+ path = inputs.get("path")
193
+ if not path:
194
+ return "Error: read_file requires 'path' parameter"
195
+ return _exec_read_file(path, inputs.get("limit"))
196
+ elif name == "write_file":
197
+ path = inputs.get("path")
198
+ content = inputs.get("content")
199
+ if not path or content is None:
200
+ return "Error: write_file requires 'path' and 'content' parameters"
201
+ return _exec_write_file(path, content)
202
+ elif name == "edit_file":
203
+ path = inputs.get("path")
204
+ old = inputs.get("old_string")
205
+ new = inputs.get("new_string")
206
+ if not all([path, old is not None, new is not None]):
207
+ return "Error: edit_file requires 'path', 'old_string', and 'new_string' parameters"
208
+ return _exec_edit_file(path, old, new)
209
+ elif name == "glob":
210
+ pattern = inputs.get("pattern")
211
+ if not pattern:
212
+ return "Error: glob requires 'pattern' parameter"
213
+ return _exec_glob(pattern, inputs.get("path", str(Path.home())))
214
+ elif name == "grep":
215
+ pattern = inputs.get("pattern")
216
+ if not pattern:
217
+ return "Error: grep requires 'pattern' parameter"
218
+ return _exec_grep(pattern, inputs.get("path", str(Path.home())), inputs.get("file_pattern"))
219
+ elif name == "list_directory":
220
+ path = inputs.get("path")
221
+ if not path:
222
+ return "Error: list_directory requires 'path' parameter"
223
+ return _exec_list_dir(path)
224
+ else:
225
+ return f"Unknown tool: {name}"
226
+ except Exception as e:
227
+ return f"Tool error ({name}): {e}"
228
+
229
+
230
+ def _exec_bash(command: str, timeout: int = 120) -> str:
231
+ """Execute bash command."""
232
+ try:
233
+ result = subprocess.run(
234
+ command,
235
+ shell=True,
236
+ capture_output=True,
237
+ text=True,
238
+ timeout=timeout,
239
+ cwd=str(Path.home())
240
+ )
241
+ output = result.stdout
242
+ if result.stderr:
243
+ output += f"\nSTDERR:\n{result.stderr}"
244
+ if result.returncode != 0:
245
+ output += f"\n[Exit code: {result.returncode}]"
246
+ return output.strip() or "(no output)"
247
+ except subprocess.TimeoutExpired:
248
+ return f"Command timed out after {timeout}s"
249
+ except Exception as e:
250
+ return f"Bash error: {e}"
251
+
252
+
253
+ def _exec_read_file(path: str, limit: Optional[int] = None) -> str:
254
+ """Read file contents."""
255
+ try:
256
+ p = Path(path).expanduser()
257
+ if not p.exists():
258
+ return f"File not found: {path}"
259
+ content = p.read_text(encoding="utf-8", errors="replace")
260
+ lines = content.splitlines()
261
+ if limit:
262
+ lines = lines[:limit]
263
+ # Add line numbers
264
+ numbered = [f"{i+1:4d} {line}" for i, line in enumerate(lines)]
265
+ return "\n".join(numbered)
266
+ except Exception as e:
267
+ return f"Read error: {e}"
268
+
269
+
270
+ def _exec_write_file(path: str, content: str) -> str:
271
+ """Write content to file."""
272
+ try:
273
+ p = Path(path).expanduser()
274
+ p.parent.mkdir(parents=True, exist_ok=True)
275
+ p.write_text(content, encoding="utf-8")
276
+ return f"Wrote {len(content)} bytes to {path}"
277
+ except Exception as e:
278
+ return f"Write error: {e}"
279
+
280
+
281
+ def _exec_edit_file(path: str, old_string: str, new_string: str) -> str:
282
+ """Edit file by replacing string."""
283
+ try:
284
+ p = Path(path).expanduser()
285
+ if not p.exists():
286
+ return f"File not found: {path}"
287
+ content = p.read_text(encoding="utf-8")
288
+ if old_string not in content:
289
+ return f"String not found in file: {old_string[:50]}..."
290
+ count = content.count(old_string)
291
+ if count > 1:
292
+ return f"String appears {count} times — be more specific"
293
+ new_content = content.replace(old_string, new_string, 1)
294
+ p.write_text(new_content, encoding="utf-8")
295
+ return f"Replaced in {path}"
296
+ except Exception as e:
297
+ return f"Edit error: {e}"
298
+
299
+
300
+ def _exec_glob(pattern: str, base_path: str) -> str:
301
+ """Find files matching glob pattern."""
302
+ try:
303
+ p = Path(base_path).expanduser()
304
+ matches = list(p.glob(pattern))[:100] # Limit results
305
+ if not matches:
306
+ return "No matches found"
307
+ return "\n".join(str(m) for m in sorted(matches))
308
+ except Exception as e:
309
+ return f"Glob error: {e}"
310
+
311
+
312
+ def _exec_grep(pattern: str, path: str, file_pattern: Optional[str] = None) -> str:
313
+ """Search with ripgrep."""
314
+ try:
315
+ cmd = ["rg", "-n", "--max-count=50", pattern, path]
316
+ if file_pattern:
317
+ cmd.extend(["-g", file_pattern])
318
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
319
+ output = result.stdout.strip()
320
+ return output if output else "No matches found"
321
+ except FileNotFoundError:
322
+ # Fallback to grep
323
+ cmd = f"grep -rn '{pattern}' {path} | head -50"
324
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
325
+ return result.stdout.strip() or "No matches found"
326
+ except Exception as e:
327
+ return f"Grep error: {e}"
328
+
329
+
330
+ def _exec_list_dir(path: str) -> str:
331
+ """List directory contents."""
332
+ try:
333
+ p = Path(path).expanduser()
334
+ if not p.exists():
335
+ return f"Directory not found: {path}"
336
+ if not p.is_dir():
337
+ return f"Not a directory: {path}"
338
+ items = sorted(p.iterdir())
339
+ lines = []
340
+ for item in items[:100]:
341
+ prefix = "d " if item.is_dir() else "f "
342
+ lines.append(f"{prefix} {item.name}")
343
+ return "\n".join(lines) if lines else "(empty directory)"
344
+ except Exception as e:
345
+ return f"List error: {e}"
346
+
347
+
348
+ class HyperClawAgent:
349
+ """Assistant agent with full tool execution."""
350
+
351
+ def __init__(self):
352
+ from hyperclaw.cost_guard import guard_client
353
+ self.client = guard_client(
354
+ anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY", "")), "anthropic")
355
+ self.system_prompt = self._load_system_prompt()
356
+ self.history: list[dict] = []
357
+
358
+ def _tools_allowed(self) -> bool:
359
+ """Tools only with a Hyper-signed QUALIFIED record (spec §11)."""
360
+ try:
361
+ from hyperclaw.qualification import is_qualified
362
+ return is_qualified("anthropic", MODEL)
363
+ except Exception:
364
+ return False
365
+
366
+ def _load_system_prompt(self) -> str:
367
+ """hyperclaw.prompt_assembly order: truth rules + access first, then
368
+ identity and persona; workspace files are fenced as DATA (bench 1.6)."""
369
+ from hyperclaw.chat_scope import OWNER
370
+ from hyperclaw.prompt_assembly import assemble, identity_block, persona_for
371
+ from hyperclaw.truth_guard import TRUTH_RULES, access_manifest
372
+ mode = "tools" if self._tools_allowed() else "chat_only"
373
+ surface = "\n".join([
374
+ "## How to work",
375
+ "- Concise when needed, thorough when it matters. No filler words.",
376
+ "- Never exfiltrate private data.",
377
+ "- When in doubt, ask before acting externally.",
378
+ "",
379
+ f"Current date: {datetime.now().strftime('%Y-%m-%d %H:%M')}",
380
+ ])
381
+ context = []
382
+ for filename in CONTEXT_FILES:
383
+ filepath = WORKSPACE_PATH / filename
384
+ if filepath.exists():
385
+ try:
386
+ context.append((filename, filepath.read_text(encoding="utf-8")))
387
+ except Exception:
388
+ pass
389
+ today = datetime.now().strftime("%Y-%m-%d")
390
+ daily_log = HYPERCLAW_ROOT / "memory" / f"{today}.md"
391
+ if daily_log.exists():
392
+ try:
393
+ context.append((f"today's log {today}", daily_log.read_text(encoding="utf-8")))
394
+ except Exception:
395
+ pass
396
+ return assemble(truth_rules=TRUTH_RULES,
397
+ access=access_manifest(mode, "anthropic", MODEL),
398
+ identity=identity_block("Assistant", "", OWNER, model=MODEL),
399
+ scope=OWNER, surface_instructions=surface,
400
+ persona=persona_for(OWNER), context=context)
401
+
402
+ def reload_context(self) -> None:
403
+ """Hot-reload context."""
404
+ self.system_prompt = self._load_system_prompt()
405
+
406
+ async def chat(self, message: str) -> AsyncIterator[str]:
407
+ """
408
+ Process a message with tool execution.
409
+ Yields text chunks as they come, executes tools as needed.
410
+ """
411
+ self.history.append({"role": "user", "content": message})
412
+
413
+ # Trim history
414
+ if len(self.history) > 120:
415
+ self.history = self.history[-120:]
416
+
417
+ messages = list(self.history)
418
+
419
+ max_iterations = 100 # Assistant: raised from 20 — complex multi-step tasks require depth
420
+ iteration = 0
421
+ consecutive_errors = 0
422
+
423
+ tools_on = self._tools_allowed() # no record -> chat-only, nothing runs
424
+ while iteration < max_iterations:
425
+ iteration += 1
426
+
427
+ try:
428
+ response = self.client.messages.create(
429
+ model=MODEL,
430
+ max_tokens=max_output("agent", "anthropic", MODEL, requested=MAX_TOKENS),
431
+ system=self.system_prompt,
432
+ tools=ADVERTISED_TOOLS if tools_on else anthropic.NOT_GIVEN,
433
+ messages=messages,
434
+ )
435
+ except anthropic.APIError as e:
436
+ yield f"[API Error: {e}]"
437
+ return
438
+ except Exception as e:
439
+ yield f"[Error: {e}]"
440
+ return
441
+
442
+ # Process response content
443
+ assistant_content = []
444
+ has_tool_use = False
445
+
446
+ for block in response.content:
447
+ if block.type == "text":
448
+ yield block.text
449
+ assistant_content.append({"type": "text", "text": block.text})
450
+ elif block.type == "tool_use" and not tools_on:
451
+ # Tools are off (no signed record): a tool_use block from
452
+ # the model never runs (fail closed).
453
+ yield (f"\n[HyperClaw: the model asked for tool '{block.name}', but tools "
454
+ f"are off on this installation; it did NOT run.]\n")
455
+ elif block.type == "tool_use":
456
+ has_tool_use = True
457
+ tool_name = block.name
458
+ tool_input = block.input
459
+ tool_id = block.id
460
+
461
+ yield f"\n[Executing: {tool_name}]\n"
462
+
463
+ # Execute the tool
464
+ result = execute_tool(tool_name, tool_input)
465
+
466
+ # Track consecutive errors to break out of loops
467
+ if result.startswith("Error:") or result.startswith("Tool error"):
468
+ consecutive_errors += 1
469
+ if consecutive_errors >= 5:
470
+ yield f"\n[Stopping: too many consecutive tool errors]\n"
471
+ return
472
+ else:
473
+ consecutive_errors = 0
474
+
475
+ # Show truncated result
476
+ if len(result) > 8000:
477
+ yield f"{result[:8000]}...\n[{len(result)} chars total — truncated]\n"
478
+ else:
479
+ yield f"{result}\n"
480
+
481
+ assistant_content.append({
482
+ "type": "tool_use",
483
+ "id": tool_id,
484
+ "name": tool_name,
485
+ "input": tool_input
486
+ })
487
+
488
+ # Add tool result to continue conversation
489
+ messages.append({"role": "assistant", "content": assistant_content})
490
+ messages.append({
491
+ "role": "user",
492
+ "content": [{
493
+ "type": "tool_result",
494
+ "tool_use_id": tool_id,
495
+ "content": result
496
+ }]
497
+ })
498
+ assistant_content = []
499
+
500
+ # If no tool use, we're done
501
+ if not has_tool_use:
502
+ # Save final assistant message to history
503
+ if assistant_content:
504
+ text_parts = [b["text"] for b in assistant_content if b["type"] == "text"]
505
+ if text_parts:
506
+ self.history.append({"role": "assistant", "content": " ".join(text_parts)})
507
+ break
508
+
509
+ # Check stop reason
510
+ if response.stop_reason == "end_turn":
511
+ break
512
+
513
+ if iteration >= max_iterations:
514
+ yield "\n[Stopped: max tool iterations reached]\n"
515
+
516
+
517
+ # Singleton
518
+ _agent: Optional[HyperClawAgent] = None
519
+
520
+
521
+ def get_agent() -> HyperClawAgent:
522
+ """Get or create the agent singleton."""
523
+ global _agent
524
+ if _agent is None:
525
+ _agent = HyperClawAgent()
526
+ return _agent
527
+
528
+
529
+ # ─────────────────────────────────────────────
530
+ # INTEGRATION TOOLS (Gmail, Calendar, iMessage, Supabase)
531
+ # ─────────────────────────────────────────────
532
+
533
+ INTEGRATION_TOOLS = [
534
+ {
535
+ "name": "gmail_inbox",
536
+ "description": "List Gmail inbox messages. Returns message IDs and snippets.",
537
+ "input_schema": {
538
+ "type": "object",
539
+ "properties": {
540
+ "max_results": {"type": "integer", "description": "Max messages to return (default 20)", "default": 20},
541
+ "query": {"type": "string", "description": "Gmail search query (default: 'in:inbox is:unread')", "default": "in:inbox is:unread"}
542
+ }
543
+ }
544
+ },
545
+ {
546
+ "name": "gmail_read",
547
+ "description": "Read the full text of a Gmail message by ID.",
548
+ "input_schema": {
549
+ "type": "object",
550
+ "properties": {
551
+ "message_id": {"type": "string", "description": "Gmail message ID"}
552
+ },
553
+ "required": ["message_id"]
554
+ }
555
+ },
556
+ {
557
+ "name": "gmail_send",
558
+ "description": "Send an email via Gmail. Always CCs the configured CC address per policy.",
559
+ "input_schema": {
560
+ "type": "object",
561
+ "properties": {
562
+ "to": {"type": "string", "description": "Recipient email address"},
563
+ "subject": {"type": "string", "description": "Email subject"},
564
+ "body": {"type": "string", "description": "Email body (plain text)"},
565
+ "cc": {"type": "string", "description": "Additional CC (optional)"},
566
+ "reply_to_id": {"type": "string", "description": "Thread ID to reply to (optional)"}
567
+ },
568
+ "required": ["to", "subject", "body"]
569
+ }
570
+ },
571
+ {
572
+ "name": "calendar_events",
573
+ "description": "Get upcoming Google Calendar events.",
574
+ "input_schema": {
575
+ "type": "object",
576
+ "properties": {
577
+ "days_ahead": {"type": "integer", "description": "How many days ahead to look (default 7)", "default": 7}
578
+ }
579
+ }
580
+ },
581
+ {
582
+ "name": "calendar_create",
583
+ "description": "Create a Google Calendar event.",
584
+ "input_schema": {
585
+ "type": "object",
586
+ "properties": {
587
+ "title": {"type": "string", "description": "Event title"},
588
+ "start_iso": {"type": "string", "description": "Start time in ISO format (e.g. 2026-03-28T14:00:00-07:00)"},
589
+ "end_iso": {"type": "string", "description": "End time in ISO format"},
590
+ "description": {"type": "string", "description": "Event description (optional)"},
591
+ "attendees": {"type": "array", "items": {"type": "string"}, "description": "List of attendee emails (optional)"}
592
+ },
593
+ "required": ["title", "start_iso", "end_iso"]
594
+ }
595
+ },
596
+ {
597
+ "name": "imessage_send",
598
+ "description": "Send an iMessage or SMS to a phone number or Apple ID email.",
599
+ "input_schema": {
600
+ "type": "object",
601
+ "properties": {
602
+ "recipient": {"type": "string", "description": "Phone number (+1XXXXXXXXXX) or Apple ID email"},
603
+ "message": {"type": "string", "description": "Message content"}
604
+ },
605
+ "required": ["recipient", "message"]
606
+ }
607
+ },
608
+ {
609
+ "name": "imessage_read",
610
+ "description": "Read recent iMessages from the Messages app database.",
611
+ "input_schema": {
612
+ "type": "object",
613
+ "properties": {
614
+ "contact": {"type": "string", "description": "Filter by contact phone/email (optional)"},
615
+ "limit": {"type": "integer", "description": "Number of messages to retrieve (default 10)", "default": 10}
616
+ }
617
+ }
618
+ },
619
+ {
620
+ "name": "supabase_query",
621
+ "description": "Query a Supabase table. Available tables: episodic_memories, hyperclaw_episodes, knowledge_nodes, kg_entities, kg_facts, hyperstates, swarm_messages, audit_log",
622
+ "input_schema": {
623
+ "type": "object",
624
+ "properties": {
625
+ "table": {"type": "string", "description": "Table name"},
626
+ "limit": {"type": "integer", "description": "Max rows (default 50)", "default": 50}
627
+ },
628
+ "required": ["table"]
629
+ }
630
+ },
631
+ {
632
+ "name": "supabase_store_memory",
633
+ "description": "Store a memory/note in Supabase episodic_memories for long-term persistence.",
634
+ "input_schema": {
635
+ "type": "object",
636
+ "properties": {
637
+ "content": {"type": "string", "description": "Memory content to store"},
638
+ "memory_type": {"type": "string", "description": "Type: episodic, semantic, procedural (default: episodic)", "default": "episodic"},
639
+ "tags": {"type": "array", "items": {"type": "string"}, "description": "Tags for retrieval"},
640
+ "importance": {"type": "number", "description": "Importance 0.0-1.0 (default 0.5)", "default": 0.5}
641
+ },
642
+ "required": ["content"]
643
+ }
644
+ },
645
+ {
646
+ "name": "integration_status",
647
+ "description": "Check the status of all Assistant integrations (Gmail, Calendar, iMessage, Supabase, Telegram, WhatsApp).",
648
+ "input_schema": {
649
+ "type": "object",
650
+ "properties": {}
651
+ }
652
+ }
653
+ ]
654
+
655
+ # Merge into main TOOLS list
656
+ TOOLS = TOOLS + INTEGRATION_TOOLS
657
+
658
+
659
+ def _exec_integration_tool(name: str, inputs: dict) -> str:
660
+ """Execute an integration tool."""
661
+ try:
662
+ from hyperclaw.integrations_layer import (
663
+ gmail_list_inbox, gmail_read_message_text, gmail_send,
664
+ calendar_get_events, calendar_create_event,
665
+ imessage_send, imessage_get_recent,
666
+ supabase_query, supabase_store_memory,
667
+ get_integration_status
668
+ )
669
+
670
+ if name == "gmail_inbox":
671
+ result = gmail_list_inbox(
672
+ inputs.get('max_results', 20),
673
+ inputs.get('query', 'in:inbox is:unread')
674
+ )
675
+ msgs = result.get('messages', [])
676
+ return f"Found {len(msgs)} messages.\nMessage IDs: {[m['id'] for m in msgs[:10]]}"
677
+
678
+ elif name == "gmail_read":
679
+ return gmail_read_message_text(inputs['message_id'])
680
+
681
+ elif name == "gmail_send":
682
+ result = gmail_send(
683
+ inputs['to'], inputs['subject'], inputs['body'],
684
+ inputs.get('cc', ''), inputs.get('reply_to_id', '')
685
+ )
686
+ return json.dumps(result)
687
+
688
+ elif name == "calendar_events":
689
+ events = calendar_get_events(inputs.get('days_ahead', 7))
690
+ if not events:
691
+ return "No upcoming events."
692
+ lines = [f"- {e['summary']} @ {e['start']}" + (f" | {e['location']}" if e.get('location') else '') for e in events]
693
+ return "\n".join(lines)
694
+
695
+ elif name == "calendar_create":
696
+ result = calendar_create_event(
697
+ inputs['title'], inputs['start_iso'], inputs['end_iso'],
698
+ inputs.get('description', ''), inputs.get('attendees', [])
699
+ )
700
+ return json.dumps(result)
701
+
702
+ elif name == "imessage_send":
703
+ result = imessage_send(inputs['recipient'], inputs['message'])
704
+ return json.dumps(result)
705
+
706
+ elif name == "imessage_read":
707
+ messages = imessage_get_recent(inputs.get('contact', ''), inputs.get('limit', 10))
708
+ if not messages:
709
+ return "No messages found."
710
+ lines = []
711
+ for m in messages:
712
+ if 'error' in m:
713
+ return f"Error: {m['error']}"
714
+ direction = "→" if m['from_me'] else "←"
715
+ lines.append(f"[{m['time']}] {direction} {m['handle']}: {m['text'][:100]}")
716
+ return "\n".join(lines)
717
+
718
+ elif name == "supabase_query":
719
+ rows = supabase_query(inputs['table'], inputs.get('limit', 50))
720
+ return json.dumps(rows[:5], indent=2, default=str)
721
+
722
+ elif name == "supabase_store_memory":
723
+ result = supabase_store_memory(
724
+ inputs['content'],
725
+ inputs.get('memory_type', 'episodic'),
726
+ inputs.get('tags', []),
727
+ inputs.get('importance', 0.5)
728
+ )
729
+ return json.dumps(result)
730
+
731
+ elif name == "integration_status":
732
+ status = get_integration_status()
733
+ return "\n".join(f"{k}: {v}" for k, v in status.items())
734
+
735
+ return f"Unknown integration tool: {name}"
736
+ except Exception as e:
737
+ return f"Integration error ({name}): {e}"
738
+
739
+
740
+ # Patch execute_tool to handle integration tools
741
+ _original_execute_tool = execute_tool
742
+
743
+ # Sends and external writes from this loop. It has no Send / Edit / Cancel
744
+ # step of its own, so they never run here (fail closed): the owner sends from
745
+ # the HyperAgents terminal or chat, where the approval layer shows the exact
746
+ # content first.
747
+ APPROVAL_REQUIRED = frozenset({"gmail_send", "calendar_create", "imessage_send",
748
+ "supabase_store_memory"})
749
+
750
+ # The only tools this loop runs: read-only ones. Shell commands and file
751
+ # writes/edits need the owner's Send / Edit / Cancel on the exact command or
752
+ # content, which this loop does not offer, so they are refused here (tier
753
+ # "never" in this loop) and not offered to the model. The HyperAgents
754
+ # terminal runs them through the approval layer instead.
755
+ READ_ONLY_TOOLS = frozenset({"read_file", "glob", "grep", "list_directory", "gmail_inbox",
756
+ "gmail_read", "calendar_events", "imessage_read", "supabase_query",
757
+ "integration_status"})
758
+ ADVERTISED_TOOLS = [t for t in TOOLS if t["name"] in READ_ONLY_TOOLS]
759
+
760
+
761
+ def execute_tool(name: str, inputs: dict) -> str:
762
+ """Execute a tool — handles both system and integration tools."""
763
+ if name in APPROVAL_REQUIRED:
764
+ return (f"Error: {name} was NOT run. Sending or writing to an outside service needs "
765
+ f"the owner's approval (Send / Edit / Cancel), which this loop does not "
766
+ f"offer. Tell the user to do it from the HyperAgents terminal or chat.")
767
+ if name not in READ_ONLY_TOOLS:
768
+ return (f"Error: not run. {name} runs a command or changes something, which needs the "
769
+ f"owner's approval (Send / Edit / Cancel) on the exact command or content; "
770
+ f"this loop does not offer that. Tell the user to do it from the HyperAgents "
771
+ f"terminal.")
772
+ integration_tools = {t['name'] for t in INTEGRATION_TOOLS}
773
+ if name in integration_tools:
774
+ return _exec_integration_tool(name, inputs)
775
+ return _original_execute_tool(name, inputs)