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.
- agents/__init__.py +31 -0
- agents/background_monitor.py +453 -0
- agents/learning.py +460 -0
- agents/planner.py +421 -0
- agents/transcriber.py +341 -0
- civilization/__init__.py +51 -0
- civilization/api/__init__.py +7 -0
- civilization/api/civilization_api.py +361 -0
- civilization/graph/__init__.py +19 -0
- civilization/graph/knowledge_linker.py +348 -0
- civilization/graph/org_graph.py +348 -0
- civilization/graph/process_graph.py +386 -0
- civilization/ingestion/__init__.py +20 -0
- civilization/ingestion/chunker.py +273 -0
- civilization/ingestion/document_ingestor.py +358 -0
- civilization/ingestion/embedder.py +262 -0
- civilization/ingestion/extractor.py +224 -0
- civilization/interview/__init__.py +19 -0
- civilization/interview/gap_detector.py +380 -0
- civilization/interview/interviewer.py +323 -0
- civilization/interview/questioner.py +363 -0
- civilization/retrieval/__init__.py +17 -0
- civilization/retrieval/civilization_rag.py +316 -0
- civilization/retrieval/context_injector.py +342 -0
- civilization/retrieval/relevance_ranker.py +287 -0
- civilization/schema.py +299 -0
- civilization/store.py +177 -0
- civilization/sync/__init__.py +16 -0
- civilization/sync/confluence_sync.py +317 -0
- civilization/sync/gdrive_sync.py +262 -0
- civilization/sync/notion_sync.py +237 -0
- civilization/versioning/__init__.py +19 -0
- civilization/versioning/diff_engine.py +351 -0
- civilization/versioning/staleness_detector.py +303 -0
- civilization/versioning/version_manager.py +293 -0
- cli/__init__.py +1 -0
- cli/doctor.py +334 -0
- cli/hyperclaw.py +1357 -0
- cli/onboarding.py +469 -0
- core/__init__.py +1 -0
- core/connectors/__init__.py +119 -0
- core/connectors/infrastructure.py +772 -0
- core/connectors/messaging.py +586 -0
- core/connectors/productivity.py +764 -0
- core/connectors/smarthome.py +528 -0
- core/connectors/social.py +730 -0
- core/hyperrouter/__init__.py +23 -0
- core/hyperrouter/bandit.py +218 -0
- core/hyperrouter/fast_loop.py +68 -0
- core/hyperrouter/slow_loop.py +143 -0
- core/hyperstate/__init__.py +24 -0
- core/hyperstate/certifier.py +154 -0
- core/hyperstate/schema.py +160 -0
- core/hyperstate/state_manager.py +103 -0
- core/hyperstate/store.py +163 -0
- core/providers/__init__.py +121 -0
- core/providers/embeddings.py +389 -0
- core/providers/image.py +534 -0
- core/providers/llm.py +1935 -0
- core/providers/search.py +608 -0
- core/providers/voice.py +474 -0
- hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
- hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
- hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
- hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
- hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
- hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
- hyperclaw/__init__.py +67 -0
- hyperclaw/__main__.py +220 -0
- hyperclaw/agent.py +775 -0
- hyperclaw/agent_coordinator.py +577 -0
- hyperclaw/anthropic_params.py +225 -0
- hyperclaw/api_utils.py +85 -0
- hyperclaw/approval_telegram.py +209 -0
- hyperclaw/approval_terminal.py +80 -0
- hyperclaw/attachments.py +177 -0
- hyperclaw/branding.py +55 -0
- hyperclaw/brief/__init__.py +11 -0
- hyperclaw/brief/cli.py +204 -0
- hyperclaw/brief/collect.py +146 -0
- hyperclaw/brief/compose.py +333 -0
- hyperclaw/brief/diff.py +110 -0
- hyperclaw/brief/errors.py +99 -0
- hyperclaw/brief/google.py +269 -0
- hyperclaw/brief/google_auth.py +294 -0
- hyperclaw/brief/links.py +124 -0
- hyperclaw/brief/service.py +424 -0
- hyperclaw/brief/store.py +205 -0
- hyperclaw/brief/tasks.py +200 -0
- hyperclaw/chat_scope.py +115 -0
- hyperclaw/cli_tui.py +497 -0
- hyperclaw/client_guard.py +717 -0
- hyperclaw/connection_check.py +113 -0
- hyperclaw/cost_guard.py +1728 -0
- hyperclaw/default_config/agents.yaml +534 -0
- hyperclaw/default_config/hyperclaw.yaml +27 -0
- hyperclaw/default_config/models.yaml +161 -0
- hyperclaw/deployment.py +39 -0
- hyperclaw/digest/__init__.py +9 -0
- hyperclaw/digest/cli.py +117 -0
- hyperclaw/digest/errors.py +100 -0
- hyperclaw/digest/gitlab.py +187 -0
- hyperclaw/digest/http.py +113 -0
- hyperclaw/digest/redact.py +32 -0
- hyperclaw/digest/service.py +271 -0
- hyperclaw/digest/slack.py +223 -0
- hyperclaw/digest/state.py +124 -0
- hyperclaw/digest/summarize.py +198 -0
- hyperclaw/digest/tokens.py +143 -0
- hyperclaw/distribution.py +46 -0
- hyperclaw/documents.py +351 -0
- hyperclaw/feeds.py +339 -0
- hyperclaw/google_workspace/__init__.py +5 -0
- hyperclaw/google_workspace/api.py +151 -0
- hyperclaw/google_workspace/auth.py +397 -0
- hyperclaw/google_workspace/calendar.py +157 -0
- hyperclaw/google_workspace/cli.py +72 -0
- hyperclaw/google_workspace/drive.py +236 -0
- hyperclaw/google_workspace/errors.py +58 -0
- hyperclaw/google_workspace/gmail.py +295 -0
- hyperclaw/google_workspace/meet.py +122 -0
- hyperclaw/google_workspace/tools.py +319 -0
- hyperclaw/hyperspeed.py +57 -0
- hyperclaw/integrations_layer.py +1427 -0
- hyperclaw/live_state.py +188 -0
- hyperclaw/managed_config.py +245 -0
- hyperclaw/media_hub.py +179 -0
- hyperclaw/memory_manager.py +584 -0
- hyperclaw/model_catalog.py +551 -0
- hyperclaw/model_policy.py +211 -0
- hyperclaw/model_router.py +595 -0
- hyperclaw/model_selector.py +72 -0
- hyperclaw/naming.py +284 -0
- hyperclaw/notices.py +65 -0
- hyperclaw/oai_stream.py +106 -0
- hyperclaw/orchestrator.py +591 -0
- hyperclaw/outbox.py +82 -0
- hyperclaw/persona.py +367 -0
- hyperclaw/prometheus.py +219 -0
- hyperclaw/prompt_assembly.py +157 -0
- hyperclaw/provider_setup.py +146 -0
- hyperclaw/providers.py +340 -0
- hyperclaw/qualification.py +341 -0
- hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
- hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
- hyperclaw/reasoning_display.py +35 -0
- hyperclaw/scheduler.py +454 -0
- hyperclaw/server.py +847 -0
- hyperclaw/setup.py +627 -0
- hyperclaw/skills.py +717 -0
- hyperclaw/solomon.py +442 -0
- hyperclaw/swarm.py +533 -0
- hyperclaw/taint.py +832 -0
- hyperclaw/team/__init__.py +28 -0
- hyperclaw/team/dispatcher.py +431 -0
- hyperclaw/team/genesis.py +106 -0
- hyperclaw/team/lead.py +110 -0
- hyperclaw/team/model.py +135 -0
- hyperclaw/team/morning.py +238 -0
- hyperclaw/team/repos.py +66 -0
- hyperclaw/team/roster.py +264 -0
- hyperclaw/team/specialist.py +235 -0
- hyperclaw/team/status.py +163 -0
- hyperclaw/team/watchers.py +112 -0
- hyperclaw/teamsetup/__init__.py +17 -0
- hyperclaw/teamsetup/checks.py +208 -0
- hyperclaw/teamsetup/item.py +391 -0
- hyperclaw/teamsetup/oauth_loopback.py +84 -0
- hyperclaw/teamsetup/onboard.py +550 -0
- hyperclaw/teamsetup/profile.py +82 -0
- hyperclaw/teamsetup/secrets.py +313 -0
- hyperclaw/teamsetup/selfcheck.py +295 -0
- hyperclaw/teamsetup/service.py +495 -0
- hyperclaw/teamsetup/slack_oauth.py +442 -0
- hyperclaw/teamsetup/tty.py +62 -0
- hyperclaw/telegram_bot.py +1106 -0
- hyperclaw/telegram_intake.py +126 -0
- hyperclaw/telegram_pairing.py +360 -0
- hyperclaw/terminal_provider.py +323 -0
- hyperclaw/tools/__init__.py +1 -0
- hyperclaw/tools/approval.py +949 -0
- hyperclaw/tools/args.py +212 -0
- hyperclaw/tools/catalog.py +244 -0
- hyperclaw/tools/classify.py +266 -0
- hyperclaw/tools/effects.py +724 -0
- hyperclaw/tools/egress.py +210 -0
- hyperclaw/tools/envelope.py +188 -0
- hyperclaw/tools/guard.py +367 -0
- hyperclaw/tools/ledger.py +339 -0
- hyperclaw/tools/receipts.py +70 -0
- hyperclaw/tools/registry.py +318 -0
- hyperclaw/tools/service_guard.py +151 -0
- hyperclaw/tools/surfaces.py +54 -0
- hyperclaw/tools/workspace.py +87 -0
- hyperclaw/trading/__init__.py +77 -0
- hyperclaw/truth_guard.py +791 -0
- hyperclaw/tts.py +124 -0
- hyperclaw/tui.py +4255 -0
- hyperclaw/tui_bridge.py +938 -0
- hyperclaw/unattended.py +60 -0
- hyperclaw/updates/__init__.py +54 -0
- hyperclaw/updates/check.py +131 -0
- hyperclaw/updates/index.py +178 -0
- hyperclaw/updates/install.py +110 -0
- hyperclaw/updates/managed.py +161 -0
- hyperclaw/updates/manifest.py +138 -0
- hyperclaw/updates/notices.py +86 -0
- hyperclaw/updates/paths.py +53 -0
- hyperclaw/updates/release.py +146 -0
- hyperclaw/updates/upgrade.py +253 -0
- hyperclaw/updates/versioning.py +104 -0
- hyperclaw/userconfig.py +119 -0
- hyperclaw/xai.py +83 -0
- integrations/__init__.py +31 -0
- integrations/automation/__init__.py +1 -0
- integrations/automation/make.py +87 -0
- integrations/automation/zapier.py +87 -0
- integrations/base.py +476 -0
- integrations/communication/__init__.py +1 -0
- integrations/communication/twilio.py +279 -0
- integrations/communication/zoom.py +290 -0
- integrations/data/__init__.py +1 -0
- integrations/data/google_analytics.py +135 -0
- integrations/data/postgres.py +182 -0
- integrations/data/supabase_connector.py +202 -0
- integrations/data/weather_feeds.py +450 -0
- integrations/developer/__init__.py +9 -0
- integrations/developer/github.py +406 -0
- integrations/developer/gitlab.py +298 -0
- integrations/enterprise/__init__.py +13 -0
- integrations/enterprise/confluence.py +266 -0
- integrations/enterprise/hubspot.py +293 -0
- integrations/enterprise/jira.py +314 -0
- integrations/enterprise/salesforce.py +286 -0
- integrations/finance/__init__.py +1 -0
- integrations/finance/polymarket_client.py +247 -0
- integrations/finance/quickbooks.py +281 -0
- integrations/finance/stripe.py +261 -0
- integrations/gateway.py +238 -0
- integrations/messaging/__init__.py +32 -0
- integrations/messaging/discord.py +159 -0
- integrations/messaging/email.py +401 -0
- integrations/messaging/imessage.py +443 -0
- integrations/messaging/signal.py +175 -0
- integrations/messaging/slack.py +217 -0
- integrations/messaging/sms.py +175 -0
- integrations/messaging/teams.py +208 -0
- integrations/messaging/telegram.py +260 -0
- integrations/messaging/whatsapp.py +203 -0
- integrations/productivity/__init__.py +17 -0
- integrations/productivity/airtable.py +217 -0
- integrations/productivity/asana.py +248 -0
- integrations/productivity/google/__init__.py +19 -0
- integrations/productivity/google/base.py +89 -0
- integrations/productivity/google/calendar.py +272 -0
- integrations/productivity/google/docs.py +202 -0
- integrations/productivity/google/drive.py +268 -0
- integrations/productivity/google/gmail.py +279 -0
- integrations/productivity/google/meet.py +218 -0
- integrations/productivity/google/sheets.py +222 -0
- integrations/productivity/google/tasks.py +199 -0
- integrations/productivity/linear.py +279 -0
- integrations/productivity/microsoft/__init__.py +13 -0
- integrations/productivity/microsoft/base.py +93 -0
- integrations/productivity/microsoft/calendar.py +207 -0
- integrations/productivity/microsoft/onedrive.py +222 -0
- integrations/productivity/microsoft/outlook.py +241 -0
- integrations/productivity/microsoft/sharepoint.py +211 -0
- integrations/productivity/notion.py +243 -0
- integrations/productivity/todoist.py +250 -0
- integrations/productivity/trello.py +244 -0
- integrations/storage/__init__.py +1 -0
- integrations/storage/box.py +279 -0
- integrations/storage/dropbox.py +215 -0
- integrations/telegram_inbound.py +179 -0
- integrations/telegram_poller.py +151 -0
- memory/__init__.py +1 -0
- memory/causal_graph.py +125 -0
- memory/impact_tracker.py +88 -0
- memory/vector_memory.py +436 -0
- models/__init__.py +12 -0
- models/chatjimmy_client.py +210 -0
- models/claude_client.py +235 -0
- models/claude_code_subagent.py +192 -0
- models/openai_client.py +120 -0
- models/router.py +162 -0
- recursive/discovery_loop.py +118 -0
- recursive/optimization_loop.py +135 -0
- recursive/validation_loop.py +38 -0
- security/__init__.py +1 -0
- security/audit_logger.py +106 -0
- security/filesystem_guard.py +100 -0
- security/hypershield.py +99 -0
- security/network_guard.py +97 -0
- security/policy_engine.py +198 -0
- swarm/__init__.py +1 -0
- swarm/agents/__init__.py +0 -0
- swarm/agents/base.py +230 -0
- swarm/agents/business/__init__.py +0 -0
- swarm/agents/business/counsel.py +31 -0
- swarm/agents/business/herald.py +32 -0
- swarm/agents/business/ledger.py +26 -0
- swarm/agents/business/nexus.py +30 -0
- swarm/agents/business/ops.py +30 -0
- swarm/agents/business/pipeline.py +28 -0
- swarm/agents/business/revenue.py +30 -0
- swarm/agents/business/sovereign.py +31 -0
- swarm/agents/business/strategos.py +27 -0
- swarm/agents/business/talent.py +27 -0
- swarm/agents/business/venture.py +32 -0
- swarm/agents/comms/__init__.py +1 -0
- swarm/agents/comms/cipher.py +32 -0
- swarm/agents/comms/echo_comms.py +31 -0
- swarm/agents/comms/envoy.py +31 -0
- swarm/agents/comms/herald_scheduler.py +33 -0
- swarm/agents/comms/pulse.py +29 -0
- swarm/agents/creative/__init__.py +0 -0
- swarm/agents/creative/author.py +27 -0
- swarm/agents/creative/lens.py +25 -0
- swarm/agents/executive/__init__.py +1 -0
- swarm/agents/intelligence/__init__.py +1 -0
- swarm/agents/intelligence/arbiter.py +30 -0
- swarm/agents/intelligence/sentinel.py +30 -0
- swarm/agents/media/__init__.py +0 -0
- swarm/agents/personal/__init__.py +0 -0
- swarm/agents/personal/atlas.py +27 -0
- swarm/agents/personal/hearth.py +26 -0
- swarm/agents/personal/midas.py +27 -0
- swarm/agents/personal/navigator.py +27 -0
- swarm/agents/personal/nourish.py +31 -0
- swarm/agents/personal/vitals.py +33 -0
- swarm/agents/recursive/__init__.py +0 -0
- swarm/agents/recursive/alchemist.py +73 -0
- swarm/agents/recursive/calibrator.py +45 -0
- swarm/agents/recursive/scout.py +97 -0
- swarm/agents/scientific/__init__.py +0 -0
- swarm/agents/scientific/cosmos.py +36 -0
- swarm/agents/scientific/gaia.py +26 -0
- swarm/agents/scientific/medicus.py +33 -0
- swarm/agents/scientific/oracle_agent.py +39 -0
- swarm/agents/scientific/scribe.py +39 -0
- swarm/agents/talent/__init__.py +0 -0
- swarm/agents/talent/deal.py +28 -0
- swarm/agents/talent/roster.py +29 -0
- swarm/agents/talent/scout.py +29 -0
- swarm/agents/talent/stage.py +27 -0
- swarm/agents/tech/__init__.py +0 -0
- swarm/agents/tech/aegis.py +28 -0
- swarm/agents/tech/bridge.py +28 -0
- swarm/agents/tech/forge.py +28 -0
- swarm/agents/trading/__init__.py +1 -0
- swarm/agents/trading/global_prediction_engine.py +611 -0
- swarm/agents/trading/polymarket_trader.py +165 -0
- swarm/agents/trading/prediction_strategist.py +218 -0
- swarm/autogen_bridge.py +68 -0
- swarm/bid_protocol.py +196 -0
- swarm/dispatcher.py +527 -0
- swarm/nexus.py +181 -0
- swarm/registry.py +112 -0
- telegram_listener.py +403 -0
- 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
|