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,235 @@
1
+ """
2
+ ClaudeClient — async Anthropic API client with retry logic, streaming, and cost tracking.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+ import logging
8
+ import time
9
+ from typing import AsyncIterator
10
+
11
+ import os
12
+
13
+ import anthropic
14
+
15
+ log = logging.getLogger("hyperclaw.claude_client")
16
+
17
+ # Cost per 1K tokens (approximate, subject to Anthropic pricing changes)
18
+ _INPUT_COST_PER_1K = 0.003
19
+ _OUTPUT_COST_PER_1K = 0.015
20
+
21
+
22
+ class ClaudeClient:
23
+ """
24
+ Async Anthropic Claude client.
25
+
26
+ Retries up to 3 times with exponential backoff on transient errors.
27
+ Logs every call: model, token counts, latency, and cost estimate.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ api_key: str,
33
+ model: str = "claude-sonnet-4-6",
34
+ max_tokens: int | None = None,
35
+ base_url: str | None = None,
36
+ ) -> None:
37
+ from hyperclaw.cost_guard import guard_client, max_output
38
+ resolved_base_url = base_url or os.environ.get("OPENAI_BASE_URL")
39
+ prov = os.environ.get("LLM_PROVIDER", "anthropic")
40
+ self._provider = prov
41
+ if prov == "bedrock":
42
+ pass # no client needed — uses boto3 per-call
43
+ elif prov == "openai_compat":
44
+ import openai as _oai
45
+ self._oai_client = guard_client(_oai.AsyncOpenAI(
46
+ api_key=api_key or os.environ.get("OPENAI_API_KEY", "placeholder"),
47
+ base_url=resolved_base_url or os.environ.get("OPENAI_BASE_URL"),
48
+ ))
49
+ else:
50
+ # anthropic or anthropic_compat
51
+ kwargs: dict = {"api_key": api_key or None}
52
+ base = resolved_base_url or os.environ.get("ANTHROPIC_BASE_URL")
53
+ if base:
54
+ kwargs["base_url"] = base
55
+ self._client = guard_client(anthropic.AsyncAnthropic(**kwargs), "anthropic")
56
+ self.model = model
57
+ # explicit max output on every call (cost guard path "chat")
58
+ self.max_tokens = max_output("chat", "anthropic", model, requested=max_tokens)
59
+
60
+ async def chat(
61
+ self,
62
+ messages: list[dict],
63
+ system: str = "",
64
+ ) -> str:
65
+ """
66
+ Send a chat request with retry logic (3 attempts, exponential backoff).
67
+ Returns the assistant's text response.
68
+ """
69
+ last_error: Exception | None = None
70
+ for attempt in range(3):
71
+ try:
72
+ return await self._do_chat(messages, system)
73
+ except (anthropic.APIConnectionError, anthropic.APITimeoutError) as e:
74
+ last_error = e
75
+ wait = 2 ** attempt
76
+ log.warning(f"Claude transient error (attempt {attempt+1}/3): {e}. Retrying in {wait}s...")
77
+ await asyncio.sleep(wait)
78
+ except anthropic.RateLimitError as e:
79
+ last_error = e
80
+ wait = 4 ** attempt
81
+ log.warning(f"Claude rate limit (attempt {attempt+1}/3). Retrying in {wait}s...")
82
+ await asyncio.sleep(wait)
83
+ raise last_error or RuntimeError("Claude chat failed after 3 attempts")
84
+
85
+ async def _do_chat(self, messages: list[dict], system: str) -> str:
86
+ if self._provider == "bedrock":
87
+ return await self._do_chat_bedrock(messages, system)
88
+ if self._provider == "openai_compat":
89
+ return await self._do_chat_openai(messages, system)
90
+ t0 = time.time()
91
+ kwargs: dict = dict(
92
+ model=self.model,
93
+ max_tokens=self.max_tokens,
94
+ messages=messages,
95
+ )
96
+ if system:
97
+ kwargs["system"] = system
98
+
99
+ response = await self._client.messages.create(**kwargs)
100
+ latency_ms = (time.time() - t0) * 1000
101
+
102
+ input_tokens = response.usage.input_tokens
103
+ output_tokens = response.usage.output_tokens
104
+ cost = (input_tokens / 1000 * _INPUT_COST_PER_1K) + (output_tokens / 1000 * _OUTPUT_COST_PER_1K)
105
+ # Handle text and thinking content blocks
106
+ text = ""
107
+ for block in (response.content or []):
108
+ if hasattr(block, "type") and block.type == "text":
109
+ text = block.text
110
+ break
111
+ if not text and response.content:
112
+ text = getattr(response.content[0], "text", "") or ""
113
+
114
+ log.info(
115
+ f"Claude call | model={self.model} | "
116
+ f"in={input_tokens} out={output_tokens} | "
117
+ f"latency={latency_ms:.0f}ms | cost=${cost:.5f}"
118
+ )
119
+ return text
120
+
121
+ async def _do_chat_bedrock(self, messages: list[dict], system: str) -> str:
122
+ try:
123
+ from core.providers.llm import BedrockProvider, Message as BMsg
124
+ except ImportError:
125
+ import sys as _sys, pathlib as _pl
126
+ _sys.path.insert(0, str(_pl.Path(__file__).parent.parent))
127
+ from core.providers.llm import BedrockProvider, Message as BMsg
128
+ t0 = time.time()
129
+ provider = BedrockProvider(
130
+ access_key=os.environ.get("AWS_ACCESS_KEY_ID"),
131
+ secret_key=os.environ.get("AWS_SECRET_ACCESS_KEY"),
132
+ region=os.environ.get("AWS_REGION", "us-east-1"),
133
+ )
134
+ bedrock_model = os.environ.get("BEDROCK_MODEL", "anthropic.claude-sonnet-5")
135
+ bmsgs = [BMsg(role=m["role"], content=m["content"]) for m in messages]
136
+ response = await provider.complete(
137
+ messages=bmsgs,
138
+ model=bedrock_model,
139
+ max_tokens=self.max_tokens,
140
+ system=system or None,
141
+ )
142
+ latency_ms = (time.time() - t0) * 1000
143
+ log.info(
144
+ f"Bedrock call | model={bedrock_model} | "
145
+ f"in={response.input_tokens} out={response.output_tokens} | "
146
+ f"latency={latency_ms:.0f}ms"
147
+ )
148
+ return response.content
149
+
150
+ async def _do_chat_openai(self, messages: list[dict], system: str) -> str:
151
+ t0 = time.time()
152
+ oai_messages = []
153
+ if system:
154
+ oai_messages.append({"role": "system", "content": system})
155
+ oai_messages.extend(messages)
156
+ model = os.environ.get("OPENAI_MODEL", self.model)
157
+ try:
158
+ response = await self._oai_client.chat.completions.create(
159
+ model=model,
160
+ messages=oai_messages,
161
+ max_tokens=self.max_tokens,
162
+ )
163
+ except Exception as _e:
164
+ _err = str(_e).lower()
165
+ if any(k in _err for k in ("connection", "connect", "unreachable", "refused", "timeout")):
166
+ log.warning(f"OpenAI-compat unreachable ({_e}), falling back to Bedrock")
167
+ import re as _re
168
+ from pathlib import Path as _Path
169
+ _bedrock_model = os.environ.get("BEDROCK_MODEL", "us.anthropic.claude-sonnet-5")
170
+ os.environ["LLM_PROVIDER"] = "bedrock"
171
+ os.environ.setdefault("AWS_REGION", "us-west-2")
172
+ os.environ.setdefault("AWS_PROFILE", "hyper")
173
+ os.environ["BEDROCK_MODEL"] = _bedrock_model
174
+ _env_f = _Path.home() / ".hyperclaw" / ".env"
175
+ if _env_f.exists():
176
+ _txt = _env_f.read_text()
177
+ _txt = _re.sub(r'^LLM_PROVIDER=.*$', 'LLM_PROVIDER=bedrock', _txt, flags=_re.MULTILINE)
178
+ _env_f.write_text(_txt)
179
+ self._provider = "bedrock"
180
+ return await self._do_chat_bedrock(messages, system)
181
+ raise
182
+ latency_ms = (time.time() - t0) * 1000
183
+ usage = response.usage
184
+ log.info(
185
+ f"OpenAI-compat call | model={model} | "
186
+ f"in={usage.prompt_tokens if usage else '?'} "
187
+ f"out={usage.completion_tokens if usage else '?'} | "
188
+ f"latency={latency_ms:.0f}ms"
189
+ )
190
+ return response.choices[0].message.content or ""
191
+
192
+ async def chat_stream(
193
+ self,
194
+ messages: list[dict],
195
+ system: str = "",
196
+ ) -> AsyncIterator[str]:
197
+ """
198
+ Stream a chat response. Yields text chunks as they arrive.
199
+ """
200
+ if self._provider == "bedrock":
201
+ # BedrockProvider has no streaming; yield full response as single chunk
202
+ text = await self._do_chat_bedrock(messages, system)
203
+ yield text
204
+ return
205
+
206
+ if self._provider == "openai_compat":
207
+ oai_messages = []
208
+ if system:
209
+ oai_messages.append({"role": "system", "content": system})
210
+ oai_messages.extend(messages)
211
+ model = os.environ.get("OPENAI_MODEL", self.model)
212
+ async with self._oai_client.chat.completions.create(
213
+ model=model,
214
+ messages=oai_messages,
215
+ max_tokens=self.max_tokens,
216
+ stream=True,
217
+ ) as stream:
218
+ async for chunk in stream:
219
+ delta = chunk.choices[0].delta.content if chunk.choices else None
220
+ if delta:
221
+ yield delta
222
+ return
223
+
224
+ # anthropic / anthropic_compat
225
+ kwargs: dict = dict(
226
+ model=self.model,
227
+ max_tokens=self.max_tokens,
228
+ messages=messages,
229
+ )
230
+ if system:
231
+ kwargs["system"] = system
232
+
233
+ async with self._client.messages.stream(**kwargs) as stream:
234
+ async for text in stream.text_stream:
235
+ yield text
@@ -0,0 +1,192 @@
1
+ """
2
+ ClaudeCodeSubagent — empirical code certification loop.
3
+ Generates code via Claude, executes it in subprocess, iterates on failures.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import asyncio
8
+ import logging
9
+ import re
10
+ import time
11
+ from dataclasses import dataclass, field
12
+
13
+ from core.hyperstate.certifier import CertificationError, Certifier
14
+ from core.hyperstate.schema import CertifiedMethod, ExperimentEntry
15
+ from .claude_client import ClaudeClient
16
+
17
+ log = logging.getLogger("hyperclaw.claude_code_subagent")
18
+
19
+ _CODE_BLOCK_RE = re.compile(r"```(?:python)?\n(.*?)```", re.DOTALL)
20
+
21
+ _GENERATE_SYSTEM = """You are a precise Python code generator.
22
+ When given a task, output ONLY a complete, runnable Python script in a ```python code block.
23
+ Include inline tests using assert statements or a simple main() function.
24
+ The code must be self-contained and executable."""
25
+
26
+ _FIX_SYSTEM = """You are a Python debugging expert.
27
+ You will be given code that failed along with its error output.
28
+ Output ONLY the fixed, complete, runnable Python script in a ```python code block.
29
+ Do not explain — just fix the code."""
30
+
31
+
32
+ @dataclass
33
+ class SubagentResult:
34
+ code: str
35
+ test_trace: str
36
+ success: bool
37
+ iterations: int
38
+ total_time_seconds: float
39
+ error: str | None = None
40
+
41
+
42
+ class ClaudeCodeSubagent:
43
+ """
44
+ Iterative code generation and certification agent.
45
+
46
+ Workflow:
47
+ 1. Ask Claude to generate code for a task
48
+ 2. Execute generated code in a subprocess (30s timeout)
49
+ 3. If tests pass → return success
50
+ 4. If tests fail → feed error back to Claude with "Fix this:" prompt
51
+ 5. Repeat up to max_iterations
52
+ 6. On final failure → return SubagentResult(success=False)
53
+ """
54
+
55
+ def __init__(self, claude_client: ClaudeClient) -> None:
56
+ self._claude = claude_client
57
+ self._certifier = Certifier()
58
+
59
+ async def run(
60
+ self,
61
+ task: str,
62
+ context: str = "",
63
+ max_iterations: int = 5,
64
+ ) -> SubagentResult:
65
+ """
66
+ Run the code generation → execution → fix loop.
67
+ Returns a SubagentResult with success/failure and full test trace.
68
+ """
69
+ t0 = time.time()
70
+ messages: list[dict] = []
71
+ code = ""
72
+ test_trace = ""
73
+ last_error: str | None = None
74
+
75
+ # Initial generation request
76
+ user_msg = f"Task: {task}"
77
+ if context:
78
+ user_msg += f"\n\nContext:\n{context}"
79
+ messages.append({"role": "user", "content": user_msg})
80
+
81
+ for iteration in range(1, max_iterations + 1):
82
+ log.info(f"Subagent iteration {iteration}/{max_iterations}")
83
+
84
+ # Generate / fix
85
+ response = await self._claude.chat(messages, system=_GENERATE_SYSTEM if iteration == 1 else _FIX_SYSTEM)
86
+ messages.append({"role": "assistant", "content": response})
87
+
88
+ # Extract code block
89
+ code = _extract_code(response)
90
+ if not code:
91
+ last_error = "No code block found in Claude response"
92
+ messages.append({"role": "user", "content": f"Fix this: {last_error}\n\nOutput a complete Python script in a ```python block."})
93
+ continue
94
+
95
+ # Execute
96
+ stdout, stderr, returncode = await _run_subprocess(code, timeout=30)
97
+ test_trace = f"=== Iteration {iteration} ===\n{stdout}"
98
+ if stderr:
99
+ test_trace += f"\n--- stderr ---\n{stderr}"
100
+ test_trace += f"\n--- exit code: {returncode} ---"
101
+
102
+ if returncode == 0:
103
+ total_time = time.time() - t0
104
+ log.info(f"Subagent succeeded on iteration {iteration} in {total_time:.1f}s")
105
+ return SubagentResult(
106
+ code=code,
107
+ test_trace=test_trace,
108
+ success=True,
109
+ iterations=iteration,
110
+ total_time_seconds=total_time,
111
+ error=None,
112
+ )
113
+ else:
114
+ last_error = f"Exit code {returncode}\nstdout: {stdout}\nstderr: {stderr}"
115
+ messages.append({
116
+ "role": "user",
117
+ "content": f"Fix this:\n\n{last_error}\n\nOutput the complete fixed Python script in a ```python block.",
118
+ })
119
+
120
+ total_time = time.time() - t0
121
+ log.warning(f"Subagent exhausted {max_iterations} iterations. Last error: {last_error}")
122
+ return SubagentResult(
123
+ code=code,
124
+ test_trace=test_trace,
125
+ success=False,
126
+ iterations=max_iterations,
127
+ total_time_seconds=total_time,
128
+ error=last_error,
129
+ )
130
+
131
+ async def certify(
132
+ self,
133
+ task: str,
134
+ context: str = "",
135
+ ) -> tuple[str, CertifiedMethod]:
136
+ """
137
+ Run the subagent loop and certify the result if successful.
138
+
139
+ Returns:
140
+ (code, CertifiedMethod) on success
141
+ Raises:
142
+ CertificationError if the run failed or certification criteria not met
143
+ """
144
+ result = await self.run(task, context)
145
+ if not result.success:
146
+ raise CertificationError(
147
+ f"Subagent failed after {result.iterations} iterations: {result.error}"
148
+ )
149
+
150
+ entry = ExperimentEntry(
151
+ method=f"claude_code_subagent:{task[:60]}",
152
+ model_used=self._claude.model,
153
+ result=result.code[:500],
154
+ certified=True,
155
+ test_trace=result.test_trace,
156
+ cost_usd=0.0,
157
+ latency_ms=result.total_time_seconds * 1000,
158
+ )
159
+ certified_method = self._certifier.certify(entry)
160
+ return result.code, certified_method
161
+
162
+
163
+ # ── Helpers ───────────────────────────────────────────────────────────────────
164
+
165
+ def _extract_code(text: str) -> str:
166
+ """Extract the first Python code block from a markdown-formatted response."""
167
+ match = _CODE_BLOCK_RE.search(text)
168
+ if match:
169
+ return match.group(1).strip()
170
+ # Fallback: return the whole response if it looks like code
171
+ if text.strip().startswith(("def ", "import ", "class ", "#")):
172
+ return text.strip()
173
+ return ""
174
+
175
+
176
+ async def _run_subprocess(code: str, timeout: float = 30.0) -> tuple[str, str, int]:
177
+ """
178
+ Execute Python code in a subprocess. Returns (stdout, stderr, returncode).
179
+ Times out after `timeout` seconds.
180
+ """
181
+ proc = await asyncio.create_subprocess_exec(
182
+ "python3", "-c", code,
183
+ stdout=asyncio.subprocess.PIPE,
184
+ stderr=asyncio.subprocess.PIPE,
185
+ )
186
+ try:
187
+ stdout_b, stderr_b = await asyncio.wait_for(proc.communicate(), timeout=timeout)
188
+ return stdout_b.decode(), stderr_b.decode(), proc.returncode or 0
189
+ except asyncio.TimeoutError:
190
+ proc.kill()
191
+ await proc.communicate()
192
+ return "", f"Process timed out after {timeout}s", 1
@@ -0,0 +1,120 @@
1
+ """
2
+ OpenAIClient — async OpenAI-compatible client supporting custom base_url.
3
+ Works with OpenAI, Azure OpenAI, local vLLM, LiteLLM, Ollama, and any
4
+ endpoint that implements the OpenAI chat completions API.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import logging
10
+ import time
11
+ from typing import AsyncIterator
12
+
13
+ from openai import AsyncOpenAI, APIConnectionError, APITimeoutError, RateLimitError
14
+
15
+ log = logging.getLogger("hyperclaw.openai_client")
16
+
17
+
18
+ class OpenAIClient:
19
+ """
20
+ Async OpenAI-compatible client with retry logic and cost tracking.
21
+
22
+ Pass base_url to point at any OpenAI-compatible endpoint:
23
+ - Azure OpenAI: https://<resource>.openai.azure.com/openai/deployments/<deployment>
24
+ - vLLM: http://localhost:8000/v1
25
+ - LiteLLM: http://localhost:4000/v1
26
+ - Ollama: http://localhost:11434/v1
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ api_key: str,
32
+ model: str = "gpt-4o",
33
+ max_tokens: int | None = None,
34
+ base_url: str | None = None,
35
+ cost_per_1k_input: float = 0.005,
36
+ cost_per_1k_output: float = 0.015,
37
+ ) -> None:
38
+ from hyperclaw.cost_guard import guard_client, max_output
39
+ self._client = guard_client(AsyncOpenAI(
40
+ api_key=api_key,
41
+ base_url=base_url,
42
+ ))
43
+ self.model = model
44
+ # explicit max output on every call (cost guard path "chat")
45
+ self.max_tokens = max_output("chat", "openai", model, requested=max_tokens)
46
+ self._cost_in = cost_per_1k_input
47
+ self._cost_out = cost_per_1k_output
48
+
49
+ async def chat(
50
+ self,
51
+ messages: list[dict],
52
+ system: str = "",
53
+ ) -> str:
54
+ """
55
+ Send a chat request with retry logic (3 attempts, exponential backoff).
56
+ Returns the assistant's text response.
57
+ """
58
+ full_messages = []
59
+ if system:
60
+ full_messages.append({"role": "system", "content": system})
61
+ full_messages.extend(messages)
62
+
63
+ last_error: Exception | None = None
64
+ for attempt in range(3):
65
+ try:
66
+ return await self._do_chat(full_messages)
67
+ except (APIConnectionError, APITimeoutError) as e:
68
+ last_error = e
69
+ wait = 2 ** attempt
70
+ log.warning(f"OpenAI transient error (attempt {attempt+1}/3): {e}. Retrying in {wait}s...")
71
+ await asyncio.sleep(wait)
72
+ except RateLimitError as e:
73
+ last_error = e
74
+ wait = 4 ** attempt
75
+ log.warning(f"OpenAI rate limit (attempt {attempt+1}/3). Retrying in {wait}s...")
76
+ await asyncio.sleep(wait)
77
+ raise last_error or RuntimeError("OpenAI chat failed after 3 attempts")
78
+
79
+ async def _do_chat(self, messages: list[dict]) -> str:
80
+ t0 = time.time()
81
+ response = await self._client.chat.completions.create(
82
+ model=self.model,
83
+ max_tokens=self.max_tokens,
84
+ messages=messages,
85
+ )
86
+ latency_ms = (time.time() - t0) * 1000
87
+
88
+ input_tokens = response.usage.prompt_tokens if response.usage else 0
89
+ output_tokens = response.usage.completion_tokens if response.usage else 0
90
+ cost = (input_tokens / 1000 * self._cost_in) + (output_tokens / 1000 * self._cost_out)
91
+ text = response.choices[0].message.content or "" if response.choices else ""
92
+
93
+ log.info(
94
+ f"OpenAI call | model={self.model} | "
95
+ f"in={input_tokens} out={output_tokens} | "
96
+ f"latency={latency_ms:.0f}ms | cost=${cost:.5f}"
97
+ )
98
+ return text
99
+
100
+ async def chat_stream(
101
+ self,
102
+ messages: list[dict],
103
+ system: str = "",
104
+ ) -> AsyncIterator[str]:
105
+ """Stream a chat response. Yields text chunks as they arrive."""
106
+ full_messages = []
107
+ if system:
108
+ full_messages.append({"role": "system", "content": system})
109
+ full_messages.extend(messages)
110
+
111
+ stream = await self._client.chat.completions.create(
112
+ model=self.model,
113
+ max_tokens=self.max_tokens,
114
+ messages=full_messages,
115
+ stream=True,
116
+ )
117
+ async for chunk in stream:
118
+ delta = chunk.choices[0].delta.content if chunk.choices else None
119
+ if delta:
120
+ yield delta