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,266 @@
1
+ """
2
+ Map a legacy tool's free-text result onto the S2 envelope.
3
+
4
+ The 87 wave-1 tools still return strings. This module turns each string into
5
+ a status by looking at fixed PREFIXES of the result (never at words anywhere
6
+ in it: a file whose text contains "not found" is still a successful read).
7
+ Tools whose outcome matters most get an explicit mapping (TOOL_RULES); every
8
+ other tool uses `generic`.
9
+
10
+ Send tools: a timeout is `uncertain` (the message may have gone out), never
11
+ `failed`, and never retried (spec rule 5).
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import os
17
+ import re
18
+ from pathlib import Path
19
+ from typing import Any, Callable, Optional
20
+
21
+ from .envelope import (REASON_CONFIRM, REASON_NOT_CONFIGURED, REASON_UNKNOWN_TOOL, Status,
22
+ ToolResult, make, new_receipt_id)
23
+
24
+ # Effects whose outcome after a timeout is unknown.
25
+ SIDE_EFFECTS = ("write", "send", "spend", "delete", "system")
26
+
27
+ _TIMEOUT = re.compile(r"\btime[d ]?\s?out\b|\btimeout\b|ReadTimeout|ConnectTimeout", re.I)
28
+ _NOT_CONFIGURED_PHRASE = re.compile(
29
+ r"not configured|\bnot available$|auth unavailable|not connected"
30
+ r"|no recipient specified|no imessage recipient|needs google oauth|missing credentials",
31
+ re.I)
32
+ _SET_VAR = re.compile(r"^Set [A-Z][A-Z0-9_]{2,}\b") # "Set SLACK_WEBHOOK_URL environment variable"
33
+
34
+
35
+ def _not_configured(text: str) -> bool:
36
+ t = text.strip()
37
+ first = t.splitlines()[0] if t else ""
38
+ return len(t) < 300 and bool(_NOT_CONFIGURED_PHRASE.search(first) or _SET_VAR.match(first))
39
+ _NOT_FOUND_PREFIX = ("not found:", "text not found", "image not found", "file not found",
40
+ "attachment not found", "no such file")
41
+ _PERM = re.compile(r"\[errno 13\]|\[errno 1\]|permission denied|operation not permitted"
42
+ r"|not authorized|not authorised|\b401\b|\b403\b", re.I)
43
+ _ENOENT = re.compile(r"\[errno 2\]|no such file or directory", re.I)
44
+ _FAIL_PREFIX = ("error", "tool error", "gmail error", "telegram error", "telegram send",
45
+ "imessage send", "email send", "forward failed", "draft failed", "failed",
46
+ "database error", "elevenlabs error", "open failed", "send failed",
47
+ "couldn't", "could not", "unable to", "traceback", "unknown action",
48
+ "messages database")
49
+
50
+
51
+ def _text(raw: Any) -> str:
52
+ if raw is None:
53
+ return ""
54
+ if isinstance(raw, str):
55
+ return raw
56
+ try:
57
+ return json.dumps(raw, default=str)
58
+ except (TypeError, ValueError):
59
+ return str(raw)
60
+
61
+
62
+ def _data(text: str) -> Any:
63
+ """Tool output as data: parsed JSON when the tool returned JSON."""
64
+ t = text.strip()
65
+ if t[:1] in ("{", "[") and t[-1:] in ("}", "]"):
66
+ try:
67
+ return json.loads(t)
68
+ except ValueError:
69
+ pass
70
+ return text
71
+
72
+
73
+ def ok(spec, text: str, *, receipt: Optional[str] = None, message: str = "") -> ToolResult:
74
+ side_effect = spec.effect != "read"
75
+ return make(Status.OK, message or f"{spec.name} completed.", data=_data(text),
76
+ receipt_id=(receipt or new_receipt_id()) if side_effect else None)
77
+
78
+
79
+ def failed(spec, text: str, *, retryable: Optional[bool] = None) -> ToolResult:
80
+ """A failure: `uncertain` for side-effecting tools that timed out."""
81
+ if _TIMEOUT.search(text):
82
+ if spec.effect in SIDE_EFFECTS:
83
+ return make(Status.UNCERTAIN, f"{spec.name} timed out; the outcome is unknown. {text}")
84
+ return make(Status.FAILED, text, retryable=True if retryable is None else retryable)
85
+ if _PERM.search(text):
86
+ return make(Status.PERMISSION_DENIED, text)
87
+ if _ENOENT.search(text):
88
+ return make(Status.NOT_FOUND, text)
89
+ return make(Status.FAILED, text, retryable=retryable)
90
+
91
+
92
+ def generic(spec, text: str, args: dict) -> ToolResult:
93
+ t = text.strip()
94
+ low = t.lower()
95
+ if low.startswith("unknown tool"):
96
+ return make(Status.NOT_FOUND, t, reason=REASON_UNKNOWN_TOOL)
97
+ if low.startswith(_NOT_FOUND_PREFIX):
98
+ return make(Status.NOT_FOUND, t)
99
+ if low.startswith("permission denied"): # e.g. a write outside the workspace
100
+ return make(Status.PERMISSION_DENIED, t)
101
+ if _not_configured(t):
102
+ return make(Status.PERMISSION_DENIED, t, reason=REASON_NOT_CONFIGURED)
103
+ if low.startswith(_FAIL_PREFIX) or (len(t) < 200 and low == "timed out"):
104
+ return failed(spec, t)
105
+ return ok(spec, text)
106
+
107
+
108
+ # ───────────────────────── explicit per-tool mappings ─────────────────────────
109
+
110
+ def _path_readable(args: dict) -> bool:
111
+ try:
112
+ p = Path(str(args.get("path", ""))).expanduser()
113
+ return p.is_file() and os.access(p, os.R_OK)
114
+ except Exception:
115
+ return False
116
+
117
+
118
+ def read_file(spec, text, args):
119
+ if text.startswith("Not found: "):
120
+ return make(Status.NOT_FOUND, f"File {args.get('path', '')} does not exist.")
121
+ if text.startswith("Error: ") and (not _path_readable(args) or "codec can't decode" in text):
122
+ return failed(spec, text)
123
+ return ok(spec, text, message=f"Read {args.get('path', '')}.")
124
+
125
+
126
+ def edit_file(spec, text, args):
127
+ path = args.get("path", "")
128
+ if text.startswith("Not found: "):
129
+ return make(Status.NOT_FOUND, f"File {path} does not exist; nothing was changed.")
130
+ if text.strip() == "Text not found":
131
+ return make(Status.NOT_FOUND, f"The text to replace was not found in {path}; the file "
132
+ f"was NOT changed.")
133
+ if text.strip() == "Edited":
134
+ return ok(spec, text, message=f"Edited {path}.")
135
+ return failed(spec, text)
136
+
137
+
138
+ def write_file(spec, text, args):
139
+ if text.startswith("Written "):
140
+ return ok(spec, text, message=f"{text} to {args.get('path', '')}.")
141
+ return failed(spec, text)
142
+
143
+
144
+ _RECEIPT = r"\[id:([^\]\s]+)\]"
145
+
146
+
147
+ def _send(success_prefixes, receipt_re=None, require_receipt=None):
148
+ """Send-tool rule. require_receipt(text) -> True when that success needs
149
+ a provider id (bench 20.3): without one it is `failed`, never `ok`."""
150
+ def rule(spec, text, args):
151
+ t = text.strip()
152
+ low = t.lower()
153
+ if any(low.startswith(p.lower()) for p in success_prefixes):
154
+ receipt = None
155
+ if receipt_re:
156
+ m = re.search(receipt_re, t)
157
+ receipt = m.group(1) if m else None
158
+ if receipt is None and require_receipt is not None and require_receipt(t):
159
+ return make(Status.FAILED,
160
+ f"{spec.name}: the provider returned no delivery receipt (message "
161
+ f"id), so it is NOT confirmed sent. Raw result: {t[:200]}")
162
+ return ok(spec, t, receipt=receipt, message=t)
163
+ if low.startswith(("file not found", "not found:")):
164
+ return make(Status.NOT_FOUND, t)
165
+ if _not_configured(t):
166
+ return make(Status.PERMISSION_DENIED, t, reason=REASON_NOT_CONFIGURED)
167
+ return failed(spec, t)
168
+ return rule
169
+
170
+
171
+ def email_mark(spec, text, args):
172
+ if "'error'" in text or '"error"' in text or text.startswith("Error"):
173
+ return failed(spec, text)
174
+ return ok(spec, text, message=f"Updated message {args.get('message_id', '')}.")
175
+
176
+
177
+ def _applescript_read(spec, text, args):
178
+ t = text.strip()
179
+ if t.startswith("Error") or "execution error" in t:
180
+ return failed(spec, t)
181
+ return generic(spec, text, args)
182
+
183
+
184
+ def calendar_read(spec, text, args):
185
+ """Unreachable is `failed`, never an empty `ok` (spec §7 calendar honesty)."""
186
+ t = text.strip()
187
+ if not t or t == "Done" or t.startswith("Error") or "execution error" in t:
188
+ return make(Status.FAILED, t or "Calendar returned nothing; it may be unreachable.")
189
+ return ok(spec, text)
190
+
191
+
192
+ def calendar_create(spec, text, args):
193
+ t = text.strip()
194
+ if t == "Event created":
195
+ return ok(spec, t, message=f"Created event '{args.get('title', '')}'.")
196
+ return failed(spec, t or "Calendar did not confirm the event.")
197
+
198
+
199
+ def email_search(spec, text, args):
200
+ return _applescript_read(spec, text, args)
201
+
202
+
203
+ _HTTP_STATUS = re.compile(r"^Status: (\d{3})")
204
+
205
+
206
+ def web_fetch(spec, text, args):
207
+ m = _HTTP_STATUS.match(text)
208
+ if not m:
209
+ return generic(spec, text, args)
210
+ code = int(m.group(1))
211
+ if code in (404, 410):
212
+ return make(Status.NOT_FOUND, f"{args.get('url', '')} returned HTTP {code}.")
213
+ if code in (401, 403):
214
+ return make(Status.PERMISSION_DENIED, f"{args.get('url', '')} returned HTTP {code}.")
215
+ if code >= 500 or code == 429:
216
+ return make(Status.FAILED, f"{args.get('url', '')} returned HTTP {code}.", retryable=True)
217
+ if code >= 400:
218
+ return make(Status.FAILED, f"{args.get('url', '')} returned HTTP {code}.")
219
+ return ok(spec, text)
220
+
221
+
222
+ def memory_forget(spec, text, args):
223
+ """Exact forget (rc3): one deletion named back, or nothing deleted."""
224
+ t = text.strip()
225
+ if t.startswith("Deleted memory "):
226
+ return make(Status.OK, t, data=t)
227
+ if t.startswith("Ambiguous:"):
228
+ return make(Status.NEEDS_APPROVAL, t, reason=REASON_CONFIRM)
229
+ if t.startswith("Memory not found"):
230
+ return make(Status.NOT_FOUND, t)
231
+ if t.startswith("Provide memory_id"):
232
+ return make(Status.INVALID_ARGS, t)
233
+ return generic(spec, text, args)
234
+
235
+
236
+ TOOL_RULES: dict = {
237
+ "read_file": read_file,
238
+ "edit_file": edit_file,
239
+ "write_file": write_file,
240
+ # Provider-confirmed receipts required where the provider gives an id (bench 20.3).
241
+ "telegram": _send(("Message sent", "Sent "), receipt_re=_RECEIPT,
242
+ require_receipt=lambda t: True),
243
+ "imessage": _send(("iMessage sent", "Sent ")), # AppleScript: no provider id exists
244
+ "slack": _send(("Message sent",)), # incoming webhook: body "ok", no id
245
+ "discord": _send(("Message sent",), receipt_re=_RECEIPT, require_receipt=lambda t: True),
246
+ "send_file": _send(("Queued ", "Sent ", "Emailed ", "Opened "), receipt_re=_RECEIPT,
247
+ require_receipt=lambda t: "via Telegram" in t or t.startswith("Emailed ")),
248
+ "email_send": _send(("Sent to",), receipt_re=_RECEIPT, require_receipt=lambda t: True),
249
+ "email_forward": _send(("Forwarded to",)),
250
+ "email_draft": _send(("Draft created",)),
251
+ "email_mark": email_mark,
252
+ "email_read": _applescript_read,
253
+ "email_search": email_search,
254
+ "calendar_read": calendar_read,
255
+ "calendar_create": calendar_create,
256
+ "web_fetch": web_fetch,
257
+ "memory_forget": memory_forget,
258
+ }
259
+
260
+
261
+ def classify(spec, raw: Any, args: Optional[dict] = None) -> ToolResult:
262
+ if isinstance(raw, ToolResult):
263
+ return raw
264
+ text = _text(raw)
265
+ rule: Callable = TOOL_RULES.get(spec.name, generic)
266
+ return rule(spec, text, args or {})