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,28 @@
1
+ """
2
+ The team of agents (HC-038, rc5).
3
+
4
+ The person's named agent is the LEAD and the only one they talk to. It plans,
5
+ hands work to specialists through ONE dispatcher (``team.dispatcher``), runs
6
+ them in parallel (at most three at once, each with a time limit) and reports
7
+ back in its own voice, naming who did what and which parts failed.
8
+
9
+ - ``roster`` the starter team (Inbox, Calendar, Slack, Research, Writer,
10
+ Repos when GitLab is connected), each with only the tools its
11
+ role needs; renamed or switched off in config / ``/team``.
12
+ - ``dispatcher`` the one dispatcher: concurrency cap, time limits, the
13
+ team-wide cost caps (one cost-guard task spans the lead and
14
+ its specialists), no specialist can start a specialist,
15
+ failures reported as failures.
16
+ - ``specialist`` one specialist's run: its own tool list, the same registry
17
+ and approval queue as the owner's chats (every send / post /
18
+ write / spend waits for Send / Edit / Cancel, labelled with
19
+ the agent's name), the truth guard, repeat guard and retry
20
+ budgets inside every run.
21
+ - ``model`` the model handle a specialist talks to (explicit max output
22
+ on every call; cost-guarded clients only).
23
+ - ``status`` ``/team``: who's doing what, today's spend, stop / pause /
24
+ resume, rename / on / off.
25
+ - ``watchers`` owner-only watcher alerts in one fixed format.
26
+ - ``morning`` the morning brief sections from whichever connectors exist.
27
+ - ``genesis`` new specialists are proposals only; nothing activates itself.
28
+ """
@@ -0,0 +1,431 @@
1
+ """
2
+ The one dispatcher.
3
+
4
+ Every hand-off from the lead (or from a scheduled job) to a specialist goes
5
+ through ``Dispatcher.run``. The legacy swarm entry points
6
+ (``hyperclaw.swarm.Swarm.dispatch`` / ``dispatch_parallel`` / ``all_hands``)
7
+ call it too; there is no second route.
8
+
9
+ Rules it enforces:
10
+
11
+ - **Specialists can't start specialists.** A run started from inside a
12
+ specialist (any thread or task that carries its context) is refused, and a
13
+ requester of the form "agent:<id>" is refused as well.
14
+ - **At most three run at once** (process-wide), **each with a time limit**.
15
+ A specialist still running at its limit is cancelled: it makes no further
16
+ model or tool call, its slot is freed, and it is reported as timed out
17
+ while the others carry on.
18
+ - **One task id spans the lead and its specialists.** The run happens inside
19
+ the caller's cost-guard task (or a new one), and every specialist thread
20
+ carries that context, so the $2 per-task and $10 per-day caps cover the
21
+ whole team; each model call is reserved before it is made.
22
+ - **Failures are failures.** An unknown or switched-off agent, a timeout, a
23
+ stop, a cost-cap stop or a fabricated claim is a failed job in the report,
24
+ with the reason, never a smoothed-over answer.
25
+ - **Pause / stop.** ``pause()`` refuses new runs until ``resume()``;
26
+ ``stop()`` cancels every running specialist.
27
+
28
+ ``live()`` is what ``/team`` shows: who is doing what, since when.
29
+ """
30
+ from __future__ import annotations
31
+
32
+ import contextvars
33
+ import logging
34
+ import threading
35
+ import time
36
+ import uuid
37
+ from dataclasses import dataclass, field
38
+ from typing import Callable, Dict, Iterable, List, Optional, Sequence
39
+
40
+ from hyperclaw.team import roster as roster_mod
41
+ from hyperclaw.team.roster import Specialist
42
+ from hyperclaw.team.specialist import (DONE, FAILED, REFUSED, STOPPED, TIMED_OUT, JobResult,
43
+ SpecialistRun)
44
+
45
+ log = logging.getLogger("hyperclaw.team.dispatcher")
46
+
47
+ # Set inside every specialist run: a dispatch from here is refused.
48
+ _IN_SPECIALIST: contextvars.ContextVar = contextvars.ContextVar("hyperclaw_team_specialist",
49
+ default=None)
50
+
51
+
52
+ def in_specialist() -> Optional[str]:
53
+ return _IN_SPECIALIST.get()
54
+
55
+
56
+ @dataclass
57
+ class Job:
58
+ agent: str # id or display name
59
+ task: str
60
+
61
+
62
+ @dataclass
63
+ class TeamReport:
64
+ task_id: str
65
+ results: List[JobResult] = field(default_factory=list)
66
+ refused: str = "" # the whole run was refused (paused, nested, ...)
67
+
68
+ @property
69
+ def ok(self) -> bool:
70
+ return not self.refused and all(r.ok for r in self.results)
71
+
72
+ @property
73
+ def origins(self) -> set:
74
+ out = set()
75
+ for r in self.results:
76
+ out |= r.origins
77
+ return out
78
+
79
+ @property
80
+ def approvals(self) -> List[str]:
81
+ return [a for r in self.results for a in r.approvals]
82
+
83
+ def text(self) -> str:
84
+ """The plain report the lead turns into its own answer: who did what,
85
+ and which parts failed, word for word."""
86
+ if self.refused:
87
+ return f"The team did not run: {self.refused}"
88
+ lines = []
89
+ for r in self.results:
90
+ if r.ok:
91
+ lines.append(f"[{r.agent_name}] {r.text}")
92
+ else:
93
+ what = {TIMED_OUT: "TIMED OUT", STOPPED: "STOPPED", REFUSED: "REFUSED"}.get(
94
+ r.status, "FAILED")
95
+ lines.append(f"[{r.agent_name}] {what}: {r.error or 'no reason recorded'}")
96
+ if r.approvals:
97
+ lines.append(f"[{r.agent_name}] waiting for the person's approval: "
98
+ f"{', '.join(r.approvals)} (nothing was sent or changed)")
99
+ return "\n".join(lines) or "No specialist ran."
100
+
101
+
102
+ @dataclass
103
+ class _Live:
104
+ task_id: str
105
+ agent_id: str
106
+ agent_name: str
107
+ task: str
108
+ started: float
109
+ cancel: threading.Event
110
+ state: str = "waiting" # waiting | running
111
+
112
+
113
+ class Dispatcher:
114
+ def __init__(self, *, handle_factory: Optional[Callable] = None, registry=None,
115
+ max_concurrent: Optional[int] = None, time_limit_s: Optional[float] = None,
116
+ team: Optional[Dict[str, Specialist]] = None, lead_name: str = ""):
117
+ self._handle_factory = handle_factory
118
+ self._registry = registry
119
+ self._max = max_concurrent
120
+ self._limit = time_limit_s
121
+ self._team = team
122
+ self.lead_name = lead_name
123
+ self._slots_lock = threading.Lock()
124
+ self._slots: Optional[threading.BoundedSemaphore] = None
125
+ self._slots_n = 0
126
+ self._paused = False
127
+ self._live: Dict[str, _Live] = {}
128
+ self._live_lock = threading.Lock()
129
+
130
+ # ── settings ──
131
+ @property
132
+ def max_concurrent(self) -> int:
133
+ n = self._max if self._max is not None else roster_mod.max_concurrent()
134
+ return max(1, min(roster_mod.MAX_CONCURRENT, int(n)))
135
+
136
+ @property
137
+ def time_limit_s(self) -> float:
138
+ return float(self._limit if self._limit is not None else roster_mod.time_limit_s())
139
+
140
+ def team(self) -> Dict[str, Specialist]:
141
+ return dict(self._team) if self._team is not None else roster_mod.roster()
142
+
143
+ def registry(self):
144
+ if self._registry is None:
145
+ from hyperclaw import tui
146
+ self._registry = tui.REGISTRY
147
+ return self._registry
148
+
149
+ def handle_factory(self):
150
+ if self._handle_factory is None:
151
+ from hyperclaw.team.model import default_handle
152
+ return default_handle
153
+ return self._handle_factory
154
+
155
+ def _semaphore(self) -> threading.BoundedSemaphore:
156
+ n = self.max_concurrent
157
+ with self._slots_lock:
158
+ if self._slots is None or self._slots_n != n:
159
+ if self._slots is not None and self.running_count():
160
+ return self._slots # never swap the semaphore under running jobs
161
+ self._slots = threading.BoundedSemaphore(n)
162
+ self._slots_n = n
163
+ return self._slots
164
+
165
+ # ── control ──
166
+ def pause(self) -> None:
167
+ self._paused = True
168
+
169
+ def resume(self) -> None:
170
+ self._paused = False
171
+
172
+ @property
173
+ def paused(self) -> bool:
174
+ return self._paused
175
+
176
+ def stop(self) -> int:
177
+ """Cancel every running or waiting specialist. Returns how many."""
178
+ with self._live_lock:
179
+ items = list(self._live.values())
180
+ for it in items:
181
+ it.cancel.set()
182
+ return len(items)
183
+
184
+ def live(self) -> List[_Live]:
185
+ with self._live_lock:
186
+ return sorted(self._live.values(), key=lambda x: x.started)
187
+
188
+ def running_count(self) -> int:
189
+ with self._live_lock:
190
+ return sum(1 for x in self._live.values() if x.state == "running")
191
+
192
+ # ── the one route ──
193
+ def run(self, jobs: Sequence[Job], *, provenance: Iterable[str] = (),
194
+ requester: str = "owner", surface: str = "chat", request: str = "",
195
+ conversation: str = "") -> TeamReport:
196
+ """Run the jobs (in parallel, at most max_concurrent at once) and
197
+ return the report. Never raises for a job's failure."""
198
+ from hyperclaw import cost_guard
199
+ nested = in_specialist()
200
+ if nested or str(requester or "").startswith("agent:"):
201
+ who = nested or str(requester).split(":", 1)[1]
202
+ return TeamReport("", refused=f"specialists can't start specialists ({who} "
203
+ f"tried to).")
204
+ if self._paused:
205
+ return TeamReport("", refused="the team is paused (/team resume to continue).")
206
+ with cost_guard.task(surface=surface, request=request or "team",
207
+ conversation=conversation) as t:
208
+ report = TeamReport(t.id)
209
+ team = self.team()
210
+ slots: List[object] = []
211
+ runnable = []
212
+ for j in jobs:
213
+ res = self._precheck(j, team)
214
+ if isinstance(res, JobResult):
215
+ slots.append(res)
216
+ else:
217
+ slots.append(len(runnable))
218
+ runnable.append((j, res))
219
+ prov = [p for p in (provenance or ()) if isinstance(p, str)]
220
+ workers = []
221
+ for j, spec in runnable:
222
+ cancel = threading.Event()
223
+ live = _Live(t.id, spec.id, spec.name, j.task, time.time(), cancel)
224
+ key = f"{t.id}:{spec.id}:{uuid.uuid4().hex[:6]}"
225
+ run = SpecialistRun(spec, j.task, handle_factory=self.handle_factory(),
226
+ registry=self.registry(), provenance=prov,
227
+ conversation=f"team:{t.id}:{spec.id}", cancel=cancel,
228
+ lead_name=self.lead_name)
229
+ workers.append(_Worker(self, key, live, run, contextvars.copy_context()))
230
+ for w in workers:
231
+ w.start()
232
+ for w in workers:
233
+ w.wait()
234
+ # Snapshots: a specialist cancelled at its limit may still be
235
+ # finishing a call on its thread; the report never changes after.
236
+ for s in slots:
237
+ report.results.append(s if isinstance(s, JobResult) else workers[s].snapshot)
238
+ return report
239
+
240
+ def _precheck(self, j: Job, team: Dict[str, Specialist]):
241
+ try:
242
+ spec = roster_mod.resolve(j.agent, team)
243
+ except roster_mod.RosterError as e:
244
+ return JobResult(str(j.agent), str(j.agent), j.task, status=FAILED, error=str(e))
245
+ if not spec.enabled:
246
+ return JobResult(spec.id, spec.name, j.task, status=REFUSED,
247
+ error=f"{spec.name} is switched off (/team on {spec.name}).")
248
+ if not (j.task or "").strip():
249
+ return JobResult(spec.id, spec.name, j.task, status=FAILED,
250
+ error="no task was given.")
251
+ return spec
252
+
253
+ def run_callables(self, jobs: Sequence[tuple], *, surface: str = "swarm",
254
+ request: str = "", requester: str = "owner") -> List[JobResult]:
255
+ """The legacy swarm's route (``hyperclaw.swarm``): each job is
256
+ (agent_id, agent_name, task, fn) where fn() returns the agent's text
257
+ or raises. Same rules as ``run``: no nesting, pause, at most three at
258
+ once, time limits, one cost-guard task, failures as failures."""
259
+ from hyperclaw import cost_guard
260
+ nested = in_specialist()
261
+ if nested or str(requester or "").startswith("agent:"):
262
+ return [JobResult(j[0], j[1], j[2], status=REFUSED,
263
+ error="specialists can't start specialists.") for j in jobs]
264
+ if self._paused:
265
+ return [JobResult(j[0], j[1], j[2], status=REFUSED,
266
+ error="the team is paused (/team resume to continue).")
267
+ for j in jobs]
268
+ with cost_guard.task(surface=surface, request=request or "team") as t:
269
+ workers = []
270
+ for aid, name, task, fn in jobs:
271
+ cancel = threading.Event()
272
+ live = _Live(t.id, aid, name, task, time.time(), cancel)
273
+ key = f"{t.id}:{aid}:{uuid.uuid4().hex[:6]}"
274
+ workers.append(_Worker(self, key, live, _CallableRun(aid, name, task, fn, cancel),
275
+ contextvars.copy_context()))
276
+ for w in workers:
277
+ w.start()
278
+ for w in workers:
279
+ w.wait()
280
+ return [w.snapshot for w in workers]
281
+
282
+ async def arun(self, jobs: Sequence[Job], **kw) -> TeamReport:
283
+ import asyncio
284
+ return await asyncio.to_thread(self.run, jobs, **kw)
285
+
286
+ # ── bookkeeping for workers ──
287
+ def _register(self, key: str, live: _Live) -> None:
288
+ with self._live_lock:
289
+ self._live[key] = live
290
+
291
+ def _unregister(self, key: str) -> None:
292
+ with self._live_lock:
293
+ self._live.pop(key, None)
294
+
295
+
296
+ class _CallableRun:
297
+ """A legacy swarm agent's run under the dispatcher's rules."""
298
+
299
+ def __init__(self, aid: str, name: str, task: str, fn: Callable[[], str],
300
+ cancel: threading.Event):
301
+ self.result = JobResult(aid, name, task)
302
+ self.fn = fn
303
+ self.cancel = cancel
304
+
305
+ def run(self) -> JobResult:
306
+ from hyperclaw.cost_guard import CostCapReached
307
+ r = self.result
308
+ r.started = time.time()
309
+ try:
310
+ text = self.fn()
311
+ if self.cancel.is_set():
312
+ r.status = STOPPED
313
+ elif not (text or "").strip():
314
+ r.status, r.error = FAILED, "it returned no answer"
315
+ else:
316
+ r.text = text
317
+ except CostCapReached as e:
318
+ r.status, r.error = FAILED, e.headline()
319
+ except Exception as e:
320
+ log.warning("swarm agent %s failed: %s", r.agent_id, type(e).__name__,
321
+ exc_info=True)
322
+ r.status, r.error = FAILED, f"it stopped with an internal error ({type(e).__name__})"
323
+ finally:
324
+ r.finished = time.time()
325
+ return r
326
+
327
+
328
+ class _Worker:
329
+ """One specialist on its own daemon thread, under a slot and a deadline."""
330
+
331
+ def __init__(self, d: Dispatcher, key: str, live: _Live, run,
332
+ ctx: contextvars.Context):
333
+ self.d, self.key, self.live, self.run, self.ctx = d, key, live, run, ctx
334
+ self.done = threading.Event()
335
+ self.started = threading.Event()
336
+ self.snapshot: Optional[JobResult] = None
337
+ self._released = False
338
+ self._rel_lock = threading.Lock()
339
+ self.sem = d._semaphore()
340
+ self.thread = threading.Thread(target=self._main, daemon=True,
341
+ name=f"hyperclaw-team-{live.agent_id}")
342
+
343
+ def start(self) -> None:
344
+ self.d._register(self.key, self.live)
345
+ self.thread.start()
346
+
347
+ def _release(self) -> None:
348
+ with self._rel_lock:
349
+ if not self._released:
350
+ self._released = True
351
+ self.sem.release()
352
+
353
+ def _main(self) -> None:
354
+ self.ctx.run(self._body)
355
+
356
+ def _body(self) -> None:
357
+ r = self.run.result
358
+ token = _IN_SPECIALIST.set(self.live.agent_id)
359
+ acquired = False
360
+ try:
361
+ while not acquired:
362
+ if self.live.cancel.is_set():
363
+ r.status, r.error = STOPPED, "it was stopped before it started."
364
+ return
365
+ acquired = self.sem.acquire(timeout=0.05)
366
+ self.live.state = "running"
367
+ self.live.started = time.time()
368
+ self.started.set()
369
+ self.run.run()
370
+ finally:
371
+ if acquired:
372
+ self._release()
373
+ _IN_SPECIALIST.reset(token)
374
+ self.done.set()
375
+ self.started.set()
376
+
377
+ def wait(self) -> None:
378
+ """Wait for the run, up to its time limit once it is running."""
379
+ limit = self.d.time_limit_s
380
+ r = self.run.result
381
+ try:
382
+ self.started.wait()
383
+ deadline = self.live.started + limit
384
+ while not self.done.is_set():
385
+ left = deadline - time.time()
386
+ if left <= 0:
387
+ self.live.cancel.set()
388
+ r.status = TIMED_OUT
389
+ r.error = f"it did not finish within its {limit:g}-second limit."
390
+ r.finished = time.time()
391
+ self.snapshot = _copy(r)
392
+ self._release() # its slot is free; it can't make another call
393
+ return
394
+ self.done.wait(min(left, 0.25))
395
+ if self.live.cancel.is_set() and r.status == DONE:
396
+ r.status = STOPPED
397
+ r.error = r.error or "it was stopped (/team stop)."
398
+ if r.status == STOPPED and not r.error:
399
+ r.error = "it was stopped (/team stop)."
400
+ self.snapshot = _copy(r)
401
+ finally:
402
+ if self.snapshot is None:
403
+ self.snapshot = _copy(r)
404
+ self.d._unregister(self.key)
405
+
406
+
407
+ def _copy(r: JobResult) -> JobResult:
408
+ from dataclasses import replace
409
+ return replace(r, tools=[dict(x) for x in r.tools], approvals=list(r.approvals),
410
+ origins=set(r.origins))
411
+
412
+
413
+ # ───────────────────────── the process-wide dispatcher ─────────────────────────
414
+
415
+ _DISPATCHER: Optional[Dispatcher] = None
416
+ _D_LOCK = threading.Lock()
417
+
418
+
419
+ def get_dispatcher() -> Dispatcher:
420
+ global _DISPATCHER
421
+ with _D_LOCK:
422
+ if _DISPATCHER is None:
423
+ _DISPATCHER = Dispatcher()
424
+ return _DISPATCHER
425
+
426
+
427
+ def set_dispatcher(d: Optional[Dispatcher]) -> None:
428
+ """Tests and embedders: replace the process-wide dispatcher."""
429
+ global _DISPATCHER
430
+ with _D_LOCK:
431
+ _DISPATCHER = d
@@ -0,0 +1,106 @@
1
+ """
2
+ GENESIS for the team: new specialists are PROPOSALS only (rc5).
3
+
4
+ A proposal is a readable package (purpose, instructions, the smallest tool
5
+ list, model tier, a cost note, example tasks, and the evidence that it is
6
+ needed). It is stored for the owner to read under ``/team proposals``.
7
+
8
+ Nothing here activates anything: there is no activation path in this
9
+ release, a proposal never changes the roster, and a proposal that asks for a
10
+ send, post, write or spend tool is refused unless the owner asked for it in
11
+ their own words (``owner_asked_for_writes``), and even then it stays a
12
+ proposal. Activation, dry runs and versioning are later work (spec §4).
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+ import secrets
19
+ import time
20
+ from dataclasses import asdict, dataclass, field
21
+ from pathlib import Path
22
+ from typing import List, Optional
23
+
24
+ PROPOSED = "proposed"
25
+ DECLINED = "declined"
26
+
27
+
28
+ @dataclass
29
+ class Proposal:
30
+ name: str
31
+ purpose: str
32
+ instructions: str
33
+ tools: List[str]
34
+ tier: str = "fast"
35
+ cost_note: str = ""
36
+ examples: List[str] = field(default_factory=list)
37
+ evidence: str = ""
38
+ id: str = ""
39
+ created: float = 0.0
40
+ status: str = PROPOSED
41
+
42
+
43
+ def _path() -> Path:
44
+ root = Path(os.environ.get("HYPERCLAW_ROOT", Path.home() / ".hyperclaw")).expanduser()
45
+ return root / "team" / "proposals.json"
46
+
47
+
48
+ def load() -> List[Proposal]:
49
+ try:
50
+ data = json.loads(_path().read_text(encoding="utf-8"))
51
+ return [Proposal(**p) for p in data if isinstance(p, dict)]
52
+ except (OSError, ValueError, TypeError):
53
+ return []
54
+
55
+
56
+ def _save(items: List[Proposal]) -> None:
57
+ p = _path()
58
+ p.parent.mkdir(parents=True, exist_ok=True)
59
+ tmp = p.with_suffix(".tmp")
60
+ tmp.write_text(json.dumps([asdict(i) for i in items], indent=2), encoding="utf-8")
61
+ os.chmod(tmp, 0o600)
62
+ os.replace(tmp, p)
63
+
64
+
65
+ class ProposalError(ValueError):
66
+ pass
67
+
68
+
69
+ def propose(name: str, purpose: str, instructions: str, tools: List[str], *,
70
+ evidence: str, examples: Optional[List[str]] = None, tier: str = "fast",
71
+ owner_asked_for_writes: bool = False, registry=None) -> Proposal:
72
+ """Store a proposal. Raises ProposalError for an unknown tool, a missing
73
+ evidence line, or a send/write tool the owner did not ask for."""
74
+ if not (evidence or "").strip():
75
+ raise ProposalError("a proposal needs evidence (what was asked, how often)")
76
+ if registry is None:
77
+ from hyperclaw import tui
78
+ registry = tui.REGISTRY
79
+ for t in tools:
80
+ spec = registry.get(t)
81
+ if spec is None:
82
+ raise ProposalError(f"unknown tool {t}")
83
+ if spec.effect != "read" and not owner_asked_for_writes:
84
+ raise ProposalError(f"{t} can {spec.effect}; a proposal gets no send or write tool "
85
+ f"unless the person asks for it")
86
+ if t in ("team_delegate", "agent_start", "agent_stop", "agent_watch"):
87
+ raise ProposalError(f"{t} is the lead's alone; a specialist cannot have it")
88
+ p = Proposal(name=name.strip(), purpose=purpose.strip(), instructions=instructions.strip(),
89
+ tools=list(tools), tier=tier, examples=list(examples or [])[:5],
90
+ evidence=evidence.strip(), id=secrets.token_hex(4), created=time.time(),
91
+ cost_note=f"model tier {tier}; runs under the same per-task and daily caps")
92
+ items = load()
93
+ items.append(p)
94
+ _save(items)
95
+ return p
96
+
97
+
98
+ def render_pending() -> str:
99
+ items = [p for p in load() if p.status == PROPOSED]
100
+ if not items:
101
+ return "No proposed specialists."
102
+ lines = ["Proposed specialists (proposals only: none of these is on your team):"]
103
+ for p in items:
104
+ lines.append(f"- [{p.id}] {p.name}: {p.purpose} Tools: {', '.join(p.tools) or 'none'}. "
105
+ f"Why: {p.evidence}")
106
+ return "\n".join(lines)
hyperclaw/team/lead.py ADDED
@@ -0,0 +1,110 @@
1
+ """
2
+ The lead's side of the team: the ``team_delegate`` tool.
3
+
4
+ Only the lead (the person's named agent, in the owner's own chats) has this
5
+ tool. It is never offered on the specialists' surface ("swarm"), never to a
6
+ guest chat, and the dispatcher refuses it from inside a specialist.
7
+
8
+ The lead's own words (the turn guard's provenance) are handed to the
9
+ specialists as THEIR provenance: a specialist can fetch an address only if
10
+ it is verbatim in what the person wrote, and can never widen what the lead
11
+ may do. The specialists' report comes back as data for the lead to answer
12
+ from in its own voice, naming who did what and which parts failed.
13
+
14
+ Connector content the specialists read (email, calendar, web, GitLab) is
15
+ recorded against the lead's conversation as connector content, per message
16
+ (``hyperclaw.taint``): the report cannot be saved to memory or learning as if
17
+ it were the person's own words.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ from typing import Any, List
23
+
24
+ MAX_ASSIGNMENTS = 6
25
+
26
+ LEAD_NOTE = (
27
+ "Answer the person in your own voice from this report. Say which specialist did each "
28
+ "part. Anything marked FAILED, TIMED OUT, STOPPED or REFUSED did not happen: say so "
29
+ "plainly. Anything waiting for approval has NOT been sent or changed.")
30
+
31
+ TOOL_SCHEMA = {
32
+ "name": "team_delegate",
33
+ "description": (
34
+ "Hand work to specialists on your team and get their report back. They run in "
35
+ "parallel (at most three at once, each with a time limit) under the same cost caps as "
36
+ "you. Each specialist has only its own tools; anything it would send, post or write "
37
+ "waits for the person's approval. Use for independent parts of a request "
38
+ "(e.g. inbox + calendar + research). Agents: inbox, calendar, slack, research, writer, "
39
+ "repos (when GitLab is connected), or their names on this person's team."),
40
+ "input_schema": {
41
+ "type": "object",
42
+ "properties": {
43
+ "assignments": {
44
+ "type": "array",
45
+ "description": "One entry per specialist: who and exactly what to do.",
46
+ "items": {
47
+ "type": "object",
48
+ "properties": {
49
+ "agent": {"type": "string", "description": "Specialist id or name"},
50
+ "task": {"type": "string",
51
+ "description": "The complete task for that specialist"},
52
+ },
53
+ "required": ["agent", "task"],
54
+ },
55
+ },
56
+ },
57
+ "required": ["assignments"],
58
+ },
59
+ }
60
+
61
+
62
+ def _assignments(args: dict) -> List[Any]:
63
+ from hyperclaw.team.dispatcher import Job
64
+ raw = args.get("assignments")
65
+ if isinstance(raw, str):
66
+ try:
67
+ raw = json.loads(raw)
68
+ except ValueError:
69
+ raw = None
70
+ if not isinstance(raw, list) or not raw:
71
+ raise ValueError("assignments must be a non-empty list of {agent, task}")
72
+ if len(raw) > MAX_ASSIGNMENTS:
73
+ raise ValueError(f"at most {MAX_ASSIGNMENTS} assignments at a time")
74
+ out = []
75
+ for a in raw:
76
+ if not isinstance(a, dict):
77
+ raise ValueError("each assignment is {agent, task}")
78
+ out.append(Job(str(a.get("agent") or ""), str(a.get("task") or "")))
79
+ return out
80
+
81
+
82
+ def delegate(args: dict, *, dispatcher=None) -> str:
83
+ """The team_delegate tool body. Returns the report text (or an error line)."""
84
+ from hyperclaw import taint
85
+ from hyperclaw.team.dispatcher import get_dispatcher
86
+ from hyperclaw.tools.registry import active_guard
87
+ try:
88
+ jobs = _assignments(args or {})
89
+ except ValueError as e:
90
+ return f"Error: {e}"
91
+ guard = active_guard()
92
+ provenance = list(getattr(guard, "provenance", None) or [])
93
+ requester = getattr(guard, "requester", "owner")
94
+ conversation = getattr(guard, "conversation", "") or ""
95
+ d = dispatcher or get_dispatcher()
96
+ try:
97
+ from hyperclaw.naming import current_name # the lead's display name, if set
98
+ d.lead_name = d.lead_name or current_name()
99
+ except Exception:
100
+ pass
101
+ rep = d.run(jobs, provenance=provenance, requester=requester,
102
+ surface=getattr(guard, "surface", "chat") or "chat",
103
+ request="; ".join(j.task for j in jobs)[:500], conversation=conversation)
104
+ if rep.refused:
105
+ return f"Error: the team did not run: {rep.refused}"
106
+ text = rep.text() + "\n\n" + LEAD_NOTE
107
+ if rep.origins and conversation:
108
+ for origin in sorted(rep.origins):
109
+ taint.mark(rep.text(), origin, "team_delegate", session=conversation)
110
+ return text