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,212 @@
1
+ """
2
+ Tool arguments: one Pydantic model per tool (tool standard S1/S4).
3
+
4
+ The model is the single source of truth: the JSON schema advertised to the
5
+ model is generated FROM it (`input_schema`), and the arguments the model
6
+ sends back are validated AGAINST it (`validate`). `extra="forbid"`, so an
7
+ argument the tool does not take is an error the model can read and correct,
8
+ never silently dropped.
9
+
10
+ Wave 1 builds the models from the existing hand-written schemas in
11
+ hyperclaw.tui.TOOLS (`model_from_schema`) so the 87 tools keep their
12
+ current inputs; a tool can later define its model by hand instead.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import functools
17
+ import json
18
+ from typing import Any, Dict, List, Literal, Optional, Tuple
19
+
20
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError, create_model
21
+
22
+ _TYPES = {
23
+ "string": str,
24
+ "integer": int,
25
+ "number": float,
26
+ "boolean": bool,
27
+ "object": Dict[str, Any],
28
+ }
29
+
30
+ # Examples used in validation messages, by argument name, then by type.
31
+ _NAME_EXAMPLES = {
32
+ "path": "~/.hyperclaw/workspace/notes/x.txt",
33
+ "image_path": "~/.hyperclaw/workspace/photo.png",
34
+ "save_path": "~/.hyperclaw/workspace/out.png",
35
+ "url": "https://example.com/page",
36
+ "query": "weather in Lisbon",
37
+ "to": "name@example.com",
38
+ "message": "On my way.",
39
+ "content": "text to save",
40
+ "command": "ls ~/Downloads",
41
+ "subject": "Meeting notes",
42
+ "body": "Hi, ...",
43
+ "title": "Team sync",
44
+ "start": "2026-10-05 10:00",
45
+ "end": "2026-10-05 11:00",
46
+ "message_id": "18c2f0a1b2c3d4e5",
47
+ "plan_id": "plan_1a2b3c",
48
+ "task_id": "task_1",
49
+ "domain": "example.com",
50
+ "location": "Lisbon",
51
+ "symbol": "AAPL",
52
+ }
53
+ _TYPE_EXAMPLES = {
54
+ "string": '"text"', "integer": "5", "number": "1.5", "boolean": "true",
55
+ "array": '["a", "b"]', "object": '{"key": "value"}',
56
+ }
57
+
58
+
59
+ class ToolArgs(BaseModel):
60
+ """Base for every tool's argument model."""
61
+ # coerce_numbers_to_str: a phone number sent as 5550100 is still a string arg.
62
+ model_config = ConfigDict(extra="forbid", coerce_numbers_to_str=True)
63
+
64
+
65
+ def _py_type(prop: dict):
66
+ if "enum" in prop and prop["enum"]:
67
+ return Literal[tuple(prop["enum"])]
68
+ t = prop.get("type", "string")
69
+ if t == "array":
70
+ items = prop.get("items") or {}
71
+ inner = _TYPES.get(items.get("type"), Any) if items else Any
72
+ return List[inner]
73
+ return _TYPES.get(t, Any)
74
+
75
+
76
+ def model_from_schema(tool_name: str, schema: dict) -> type:
77
+ """Build the argument model for a tool from its JSON schema."""
78
+ props = (schema or {}).get("properties", {}) or {}
79
+ required = set((schema or {}).get("required", []) or [])
80
+ fields = {}
81
+ for pname, prop in props.items():
82
+ typ = _py_type(prop)
83
+ desc = prop.get("description")
84
+ if pname in required:
85
+ fields[pname] = (typ, Field(..., description=desc))
86
+ else:
87
+ default = prop.get("default")
88
+ fields[pname] = (Optional[typ], Field(default, description=desc))
89
+ cls_name = "".join(p.capitalize() for p in tool_name.split("_")) + "Args"
90
+ return create_model(cls_name, __base__=ToolArgs, **fields)
91
+
92
+
93
+ def _clean(node, in_properties: bool = False):
94
+ """Make Pydantic's JSON schema read like a hand-written tool schema:
95
+ no titles, Optional[X] shown as X, no null defaults. (Keys directly under
96
+ "properties" are argument names, so an argument called "title" stays.)"""
97
+ if isinstance(node, list):
98
+ return [_clean(x) for x in node]
99
+ if not isinstance(node, dict):
100
+ return node
101
+ if in_properties:
102
+ return {k: _clean(v) for k, v in node.items()}
103
+ out = {}
104
+ for k, v in node.items():
105
+ if k == "title":
106
+ continue
107
+ out[k] = _clean(v, in_properties=(k == "properties"))
108
+ any_of = out.get("anyOf")
109
+ if isinstance(any_of, list):
110
+ non_null = [x for x in any_of if x != {"type": "null"}]
111
+ if len(non_null) == 1 and len(non_null) < len(any_of):
112
+ del out["anyOf"]
113
+ merged = dict(non_null[0])
114
+ merged.update(out)
115
+ out = merged
116
+ if out.get("default", 0) is None:
117
+ del out["default"]
118
+ return out
119
+
120
+
121
+ @functools.lru_cache(maxsize=None)
122
+ def _input_schema_json(model: type) -> str:
123
+ schema = _clean(model.model_json_schema())
124
+ schema.setdefault("type", "object")
125
+ schema.setdefault("properties", {})
126
+ return json.dumps(schema)
127
+
128
+
129
+ def input_schema(model: type) -> dict:
130
+ """The JSON schema advertised to the model, generated from `model`
131
+ (a fresh copy each call)."""
132
+ return json.loads(_input_schema_json(model))
133
+
134
+
135
+ # ───────────────────────── validation ─────────────────────────
136
+
137
+ class ArgsError(Exception):
138
+ """Arguments that cannot be used; str(self) is the model-readable message."""
139
+
140
+
141
+ def _field_type(model: type, name: str) -> Tuple[str, Optional[list]]:
142
+ prop = input_schema(model).get("properties", {}).get(name, {})
143
+ return prop.get("type", "string"), prop.get("enum")
144
+
145
+
146
+ def _example(name: str, typ: str, enum: Optional[list]) -> str:
147
+ if enum:
148
+ return json.dumps(enum[0])
149
+ if typ == "string" and name in _NAME_EXAMPLES:
150
+ return _NAME_EXAMPLES[name]
151
+ return _TYPE_EXAMPLES.get(typ, '"text"')
152
+
153
+
154
+ def call_example(model: type) -> str:
155
+ """A minimal valid call shape, e.g. {"path": "..."}."""
156
+ schema = input_schema(model)
157
+ req = schema.get("required", []) or []
158
+ props = schema.get("properties", {})
159
+ parts = []
160
+ for name in req:
161
+ typ = props.get(name, {}).get("type", "string")
162
+ ex = _example(name, typ, props.get(name, {}).get("enum"))
163
+ if typ == "string" and not ex.startswith('"'):
164
+ ex = json.dumps(ex)
165
+ parts.append(f'"{name}": {ex}')
166
+ return "{" + ", ".join(parts) + "}"
167
+
168
+
169
+ def _describe(model: type, err: dict) -> str:
170
+ loc = err.get("loc") or ()
171
+ name = str(loc[0]) if loc else "arguments"
172
+ sub = "".join(f"[{x}]" if isinstance(x, int) else f".{x}" for x in loc[1:])
173
+ kind = err.get("type", "")
174
+ valid = list(input_schema(model).get("properties", {}))
175
+ if kind == "extra_forbidden":
176
+ return (f"'{name}' is not an argument of this tool (valid arguments: "
177
+ f"{', '.join(valid) or 'none'})")
178
+ typ, enum = _field_type(model, name)
179
+ ex = _example(name, typ, enum)
180
+ if kind == "missing":
181
+ return f"'{name}' is required ({typ}, e.g. {ex})"
182
+ if enum and kind in ("literal_error", "enum"):
183
+ return f"'{name}' must be one of: {', '.join(map(str, enum))}"
184
+ got = err.get("input")
185
+ got_type = type(got).__name__ if got is not None else "null"
186
+ shown = json.dumps(got, default=str)
187
+ if len(shown) > 60:
188
+ shown = shown[:57] + "..."
189
+ return f"'{name}{sub}' must be {typ}, got {got_type} {shown} (e.g. {ex})"
190
+
191
+
192
+ def validate(model: type, raw: Any) -> dict:
193
+ """Validate raw tool-call arguments. Returns the arguments the caller
194
+ actually supplied (coerced), or raises ArgsError with a message that
195
+ names each bad field, its expected type and an example."""
196
+ if isinstance(raw, (str, bytes)):
197
+ try:
198
+ raw = json.loads(raw) if raw else {}
199
+ except ValueError as e:
200
+ raise ArgsError(f"arguments are not valid JSON ({e}). Send one JSON object, "
201
+ f"e.g. {call_example(model)}")
202
+ if raw is None:
203
+ raw = {}
204
+ if not isinstance(raw, dict):
205
+ raise ArgsError(f"arguments must be a JSON object, got {type(raw).__name__}. "
206
+ f"e.g. {call_example(model)}")
207
+ try:
208
+ inst = model.model_validate(raw)
209
+ except ValidationError as e:
210
+ problems = [_describe(model, err) for err in e.errors()[:5]]
211
+ raise ArgsError("; ".join(problems))
212
+ return inst.model_dump(exclude_unset=True, exclude_none=True)
@@ -0,0 +1,244 @@
1
+ """
2
+ Registry entries for the 87 wave-1 tools.
3
+
4
+ Generated from the existing hand-written schemas (hyperclaw.tui.TOOLS) so no
5
+ tool's inputs or logic change; effect / scope / tier are set by hand below
6
+ (TOOL-INVENTORY-2026-10-02 §3a). A tool missing from EFFECT fails closed:
7
+ effect "system", tier "ask", owner scope.
8
+
9
+ Wave-1 status of the fields:
10
+ - effect, scope, tier: declared for every tool. `never_from_chat` and guest
11
+ scope are ENFORCED (surfaces). `ask` is ENFORCED from wave 3: the call is
12
+ queued for the owner's Send / Edit / Cancel (tools/approval.py) and runs
13
+ only after Send, with exactly the approved payload. Every tool whose
14
+ effect is not "read" is `ask`, `never_from_chat` (gated on the terminal
15
+ too) or `never` (refused everywhere).
16
+ - retry_budget, idempotent_on, target_on: ENFORCED from wave 2 by
17
+ tools/guard.py (effect ledger + per-tool / per-resource budgets).
18
+ """
19
+ from __future__ import annotations
20
+
21
+ from typing import Callable, Iterable
22
+
23
+ from hyperclaw.chat_scope import GUEST_SAFE_TOOLS
24
+ from hyperclaw.google_workspace import tools as _gws
25
+
26
+ from .args import model_from_schema
27
+ from .registry import PUBLIC, ToolRegistry, ToolSpec
28
+ from .surfaces import NEVER_FROM_CHAT
29
+
30
+ # ───────────────────────── effect (side-effect class) ─────────────────────────
31
+ _READ = {
32
+ "read_file", "web_fetch", "web_search", "screenshot", "clipboard_read", "imessage_read",
33
+ "email_read", "email_search", "calendar_read", "wikipedia", "weather", "news",
34
+ "stock_price", "translate", "youtube", "reddit", "hackernews", "arxiv", "math",
35
+ "time_info", "dns_lookup", "whois", "hash", "uuid_generate", "base64_encode",
36
+ "json_format", "regex_test", "pdf_read", "document_read", "geocode", "currency", "unit_convert",
37
+ "wayback", "memory_search", "memory_list", "memory_stats", "agent_status",
38
+ "swarm_roster", "plan_status", "plan_next", "plan_list", "transcripts_list",
39
+ "learning_stats", "learning_advice",
40
+ # Team of agents (HC-038): the lead hands work to specialists (their own
41
+ # sends/writes are gated in their runs), and the Repos specialist's
42
+ # read-only GitLab review queue.
43
+ "team_delegate", "gitlab_review_queue",
44
+ }
45
+ _WRITE = {
46
+ "write_file", "edit_file", "download", "clipboard_write", "memory_store", "qr_code",
47
+ "calendar_create", "email_draft", "email_mark", "github", "sql_query", "plan_create",
48
+ "plan_update", "learning_log", "learning_feedback", "agent_watch", "transcribe_audio",
49
+ "transcribe_video", "document_create",
50
+ # Not read-only: it saves each reflection as a new row (agents/learning.py
51
+ # reflect() INSERTs into reflections). Found by the catalog-wide test.
52
+ "learning_reflect",
53
+ }
54
+ _SEND = {"telegram", "imessage", "send_file", "email_send", "email_forward", "slack",
55
+ "discord", "http_request",
56
+ # Not read-only: it uploads the image file's bytes to a model provider's
57
+ # API (tui.vision), which may be a different provider than the chat's.
58
+ "vision"}
59
+ # Google Workspace (hyperclaw/google_workspace/tools.py): reads auto, the rest ask.
60
+ _READ |= set(_gws.READ)
61
+ _WRITE |= set(_gws.WRITE)
62
+ _SEND |= set(_gws.SEND)
63
+ _SPEND = {"image_generate"}
64
+ _DELETE = {"memory_forget"}
65
+ _SYSTEM = {"bash", "python_exec", "applescript", "type_text", "key_press", "mouse_click",
66
+ "open_app", "open_file", "open_url", "record_audio", "notification", "speak",
67
+ "agent_start", "agent_stop"}
68
+
69
+ EFFECT = {}
70
+ for _effect, _names in (("read", _READ), ("write", _WRITE), ("send", _SEND),
71
+ ("spend", _SPEND), ("delete", _DELETE), ("system", _SYSTEM)):
72
+ for _n in _names:
73
+ assert _n not in EFFECT, _n
74
+ EFFECT[_n] = _effect
75
+
76
+ # ───────────────────────── tier (cross-cutting rule 4) ─────────────────────────
77
+ # Vaughn's rule: "every send, post or write shows the exact content and waits
78
+ # for Send / Edit / Cancel"; "a tool marked read-only must never send data".
79
+ #
80
+ # ask: EVERY tool that is not read-only: sends, posts, writes (files, memory,
81
+ # clipboard, calendar, email, plans: bench 15.6 "task changes are proposed,
82
+ # not applied"), deletes, spend, and local actions (notifications, speech,
83
+ # background-agent control). The card shows the exact payload.
84
+ # never_from_chat: shell / code execution and desktop or app control
85
+ # (surfaces.NEVER_FROM_CHAT). Removed from every chat surface; on the local
86
+ # terminal they are gated too (tools/approval.py): the card shows the exact
87
+ # command line / script / keystrokes / click / app / file / address.
88
+ # never: refused on every surface, because what it would do cannot be shown
89
+ # up front (NEVER_REASON). Not offered to any model.
90
+ NEVER_REASON: dict = {
91
+ # agent_start was here until rc5. Watchers now only ever message their
92
+ # owner, in one fixed one-line format, never open links and never act
93
+ # (hyperclaw/team/watchers.py), so what they would send CAN be shown up
94
+ # front: agent_start is an `ask` tool whose card says exactly that.
95
+ }
96
+ NEVER = frozenset(NEVER_REASON)
97
+ ASK = frozenset(n for n, e in EFFECT.items()
98
+ if e != "read" and n not in NEVER_FROM_CHAT and n not in NEVER)
99
+
100
+ # ───────────────────────── scope (whose data/device it touches) ─────────────────────────
101
+ SCOPE = {}
102
+ for _n in GUEST_SAFE_TOOLS:
103
+ SCOPE[_n] = PUBLIC
104
+ for _n in ("read_file", "write_file", "edit_file", "download", "pdf_read", "vision", "qr_code",
105
+ "document_read", "document_create",
106
+ "image_generate", "transcribe_audio", "transcribe_video", "transcripts_list",
107
+ "sql_query"):
108
+ SCOPE[_n] = "owner.files"
109
+ for _n in ("memory_store", "memory_search", "memory_forget", "memory_list", "memory_stats",
110
+ "learning_log", "learning_stats", "learning_reflect", "learning_advice",
111
+ "learning_feedback", "plan_create", "plan_status", "plan_next", "plan_update",
112
+ "plan_list"):
113
+ SCOPE[_n] = "owner.memory"
114
+ for _n in ("email_read", "email_send", "email_search", "email_forward", "email_draft",
115
+ "email_mark"):
116
+ SCOPE[_n] = "owner.email"
117
+ for _n in ("calendar_read", "calendar_create"):
118
+ SCOPE[_n] = "owner.calendar"
119
+ for _n in ("telegram", "imessage", "imessage_read", "send_file", "slack", "discord"):
120
+ SCOPE[_n] = "owner.messages"
121
+ for _n in ("web_fetch", "http_request"):
122
+ SCOPE[_n] = "owner.network" # can reach the owner's local network
123
+ SCOPE["github"] = "owner.accounts"
124
+ SCOPE["gitlab_review_queue"] = "owner.accounts"
125
+ SCOPE["team_delegate"] = "owner.team"
126
+ SCOPE.update(_gws.SCOPE)
127
+ # Everything else (desktop, clipboard, audio, agents) defaults to owner.device.
128
+
129
+ # ───────────────────────── retry budget / idempotency (enforced; wave 2) ─────────────────────────
130
+ _NETWORK_READ = {"web_fetch", "web_search", "email_read", "email_search", "calendar_read",
131
+ "wikipedia", "weather", "news", "stock_price", "translate", "youtube",
132
+ "reddit", "hackernews", "arxiv", "dns_lookup", "whois", "geocode",
133
+ "currency", "wayback", "gitlab_review_queue"} | set(_gws.READ)
134
+
135
+ IDEMPOTENT_ON = {
136
+ "write_file": ("path", "content"),
137
+ "edit_file": ("path", "old_text", "new_text"),
138
+ "download": ("url", "path"),
139
+ "clipboard_write": ("text",),
140
+ "memory_store": ("content", "domain"),
141
+ "memory_forget": ("memory_id", "query"),
142
+ "qr_code": ("data", "save_path"),
143
+ "image_generate": ("prompt", "size", "save_path"),
144
+ "telegram": ("message", "file"),
145
+ "imessage": ("recipient", "message", "file"),
146
+ "send_file": ("path", "via", "to"),
147
+ "email_send": ("to", "cc", "bcc", "subject", "body", "attachments"),
148
+ "email_forward": ("message_id", "to"),
149
+ "email_draft": ("to", "subject", "body"),
150
+ "email_mark": ("message_id", "mark_read", "archive", "star"),
151
+ "calendar_create": ("title", "start", "end"),
152
+ "slack": ("channel", "message"),
153
+ "discord": ("message",),
154
+ "http_request": ("method", "url", "body", "json_body"),
155
+ "github": ("action", "owner", "repo", "title"),
156
+ "sql_query": ("database", "query"),
157
+ "plan_create": ("goal",),
158
+ "document_create": ("format", "title", "notes", "path"),
159
+ **_gws.IDEMPOTENT_ON,
160
+ }
161
+
162
+ # The idempotent_on fields that are the effect's TARGET; the remaining fields
163
+ # are its payload. Same target + different payload inside the conflict window
164
+ # is held (needs_approval) for send/spend/delete. A single path field makes
165
+ # the target "file:<resolved path>", shared with bash redirects.
166
+ TARGET_ON = {
167
+ "write_file": ("path",),
168
+ "edit_file": ("path",),
169
+ "download": ("path",),
170
+ "qr_code": ("save_path",),
171
+ "image_generate": ("prompt",),
172
+ "clipboard_write": (),
173
+ "memory_store": ("domain",),
174
+ "memory_forget": ("memory_id", "query"),
175
+ "telegram": (), # the owner's own chat
176
+ "imessage": ("recipient",),
177
+ "send_file": ("via", "to"),
178
+ "email_send": ("to", "cc", "bcc"),
179
+ "email_forward": ("message_id", "to"),
180
+ "email_draft": ("to", "subject"),
181
+ "email_mark": ("message_id",),
182
+ "calendar_create": ("title", "start"),
183
+ "slack": ("channel",),
184
+ "discord": (),
185
+ "http_request": ("method", "url"),
186
+ "github": ("action", "owner", "repo"),
187
+ "sql_query": ("database",),
188
+ "plan_create": (),
189
+ "document_create": ("path",),
190
+ **_gws.TARGET_ON,
191
+ }
192
+ for _n, _t in TARGET_ON.items():
193
+ assert set(_t) <= set(IDEMPOTENT_ON[_n]), _n
194
+
195
+ # ───────────────────────── descriptions (S1: name the one tool per effect) ─────────────────────────
196
+ DESCRIPTION_OVERRIDES = {
197
+ "bash": ("Execute a bash command (local terminal only). Never use bash to read, write or "
198
+ "edit files: use read_file, write_file or edit_file."),
199
+ "read_file": ("Read contents of a file. To read a file use read_file. If the result is "
200
+ "not_found, do not search for the file with other tools; ask the user where "
201
+ "it is."),
202
+ "write_file": ("Write content to a file (creates or overwrites it). To save a file use "
203
+ "write_file, once. Never use bash to write files."),
204
+ "edit_file": ("Edit a file by replacing old text with new text. To change part of a file "
205
+ "use edit_file; never use bash for edits."),
206
+ }
207
+
208
+
209
+ def _retry_budget(name: str, effect: str) -> int:
210
+ if effect != "read" or name == "team_delegate":
211
+ return 0
212
+ return 1 if name in _NETWORK_READ else 2
213
+
214
+
215
+ def build_specs(tools: Iterable[dict], handler: Callable[[str, dict], object]) -> list:
216
+ specs = []
217
+ for t in tools:
218
+ name = t["name"]
219
+ effect = EFFECT.get(name, "system")
220
+ if name in NEVER:
221
+ tier = "never"
222
+ elif name in NEVER_FROM_CHAT:
223
+ tier = "never_from_chat"
224
+ elif name in ASK or name not in EFFECT:
225
+ tier = "ask"
226
+ else:
227
+ tier = "auto"
228
+ specs.append(ToolSpec(
229
+ name=name,
230
+ description=DESCRIPTION_OVERRIDES.get(name, t.get("description", "")),
231
+ args_model=model_from_schema(name, t.get("input_schema", {})),
232
+ effect=effect,
233
+ scope=SCOPE.get(name, "owner.device"),
234
+ tier=tier,
235
+ retry_budget=_retry_budget(name, effect),
236
+ idempotent_on=IDEMPOTENT_ON.get(name, ()),
237
+ target_on=TARGET_ON.get(name, ()),
238
+ handler=handler,
239
+ ))
240
+ return specs
241
+
242
+
243
+ def build_registry(tools: Iterable[dict], handler: Callable[[str, dict], object]) -> ToolRegistry:
244
+ return ToolRegistry(build_specs(tools, handler))