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,225 @@
|
|
|
1
|
+
"""Thinking / effort / max_tokens parameters for Anthropic Messages requests.
|
|
2
|
+
|
|
3
|
+
One place decides how HyperClaw asks a Claude model to think. Call sites
|
|
4
|
+
never write ``thinking=`` or ``budget_tokens`` themselves; they call
|
|
5
|
+
:func:`thinking_params` with the model id and splat the result into
|
|
6
|
+
``client.messages.create`` / ``client.messages.stream``.
|
|
7
|
+
|
|
8
|
+
Why: models differ, and sending the wrong shape is a hard 400. The 5.x
|
|
9
|
+
generation (Claude Opus 5.5, Claude Sonnet 5.5, Fable 5/5.1, Opus 5,
|
|
10
|
+
Sonnet 5, Opus 4.7/4.8) rejects ``thinking: {type: "enabled",
|
|
11
|
+
budget_tokens: N}``; they take ``thinking: {type: "adaptive"}`` with depth
|
|
12
|
+
set by ``output_config.effort``. Older models (Haiku 4.5, Sonnet/Opus 4.5
|
|
13
|
+
and earlier) only think with ``enabled`` + ``budget_tokens`` (budget >=
|
|
14
|
+
1024 and < ``max_tokens``). Models without thinking get no thinking param.
|
|
15
|
+
|
|
16
|
+
The capability table below is data. Per-model overrides live in
|
|
17
|
+
``models.yaml`` under ``models.<model id>``::
|
|
18
|
+
|
|
19
|
+
models:
|
|
20
|
+
claude-sonnet-5-5:
|
|
21
|
+
thinking: adaptive # adaptive | enabled | none
|
|
22
|
+
effort: high # low | medium | high | xhigh | max
|
|
23
|
+
thinking_budget: 8000 # only used with thinking: enabled
|
|
24
|
+
|
|
25
|
+
An override can never ask a model for a mode it rejects: ``enabled`` on a
|
|
26
|
+
model without ``budget_tokens`` support falls back to ``adaptive``, and an
|
|
27
|
+
effort level the model does not list is ignored (both logged).
|
|
28
|
+
|
|
29
|
+
Unknown models (not in the table, no override): thinking and effort are
|
|
30
|
+
OMITTED. Omitting ``thinking`` is accepted by every Claude model — current
|
|
31
|
+
models then think adaptively by default, older ones answer without
|
|
32
|
+
thinking — so an unknown id can never 400 on these parameters.
|
|
33
|
+
|
|
34
|
+
Model ids are normalised before lookup, so Bedrock (``anthropic.…``,
|
|
35
|
+
``us.anthropic.…``, ``-v1:0``), Vertex (``@2025…``), dated snapshots
|
|
36
|
+
(``-20251001``) and ``[1m]`` suffixes all resolve to the same entry.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import logging
|
|
42
|
+
import re
|
|
43
|
+
from dataclasses import dataclass
|
|
44
|
+
from typing import Optional
|
|
45
|
+
|
|
46
|
+
log = logging.getLogger("hyperclaw.anthropic_params")
|
|
47
|
+
|
|
48
|
+
THINKING_MODES = ("adaptive", "enabled", "none")
|
|
49
|
+
ALL_EFFORTS = ("low", "medium", "high", "xhigh", "max")
|
|
50
|
+
_E5 = ALL_EFFORTS
|
|
51
|
+
_E46 = ("low", "medium", "high", "max")
|
|
52
|
+
|
|
53
|
+
DEFAULT_THINKING_BUDGET = 10000
|
|
54
|
+
MIN_THINKING_BUDGET = 1024 # API minimum for budget_tokens
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class ModelThinking:
|
|
59
|
+
mode: str # adaptive | enabled | none
|
|
60
|
+
effort: Optional[str] = None # default output_config.effort (None = omit)
|
|
61
|
+
efforts: tuple = () # effort levels the model accepts
|
|
62
|
+
budget_tokens: bool = False # accepts thinking.type "enabled" + budget
|
|
63
|
+
display: Optional[str] = None # thinking.display to request (adaptive only)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _adaptive(effort=None, efforts=_E5, display="summarized"):
|
|
67
|
+
return ModelThinking("adaptive", effort, efforts, False, display)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_LEGACY = ModelThinking("enabled", None, (), True, None)
|
|
71
|
+
_NONE = ModelThinking("none", None, (), False, None)
|
|
72
|
+
|
|
73
|
+
# Capability table. Keys are normalised ids (see normalize_model_id).
|
|
74
|
+
# display="summarized" is requested where the API default is "omitted"
|
|
75
|
+
# (empty thinking text), so the TUI's visible-thinking stream has content.
|
|
76
|
+
CAPABILITIES: dict = {
|
|
77
|
+
# 5.5 generation: adaptive + explicit effort. Opus 5.5's API default is
|
|
78
|
+
# "medium" (one below Opus 5), so it is set explicitly; Sonnet 5.5 levels
|
|
79
|
+
# are recalibrated and "medium" is the documented starting point for
|
|
80
|
+
# agentic, multistep tool use.
|
|
81
|
+
"claude-opus-5-5": _adaptive(effort="high"),
|
|
82
|
+
"claude-sonnet-5-5": _adaptive(effort="medium"),
|
|
83
|
+
# Thinking always on; enabled/disabled are 400s.
|
|
84
|
+
"claude-fable-5-1": _adaptive(),
|
|
85
|
+
"claude-mythos-5-1": _adaptive(),
|
|
86
|
+
"claude-fable-5": _adaptive(),
|
|
87
|
+
"claude-mythos-5": _adaptive(),
|
|
88
|
+
# Adaptive only; budget_tokens is a 400.
|
|
89
|
+
"claude-opus-5": _adaptive(),
|
|
90
|
+
"claude-sonnet-5": _adaptive(),
|
|
91
|
+
"claude-opus-4-8": _adaptive(),
|
|
92
|
+
"claude-opus-4-7": _adaptive(),
|
|
93
|
+
# 4.6: adaptive recommended (budget_tokens deprecated); no xhigh; the
|
|
94
|
+
# API already returns summarized thinking, so no display override.
|
|
95
|
+
"claude-opus-4-6": _adaptive(efforts=_E46, display=None),
|
|
96
|
+
"claude-sonnet-4-6": _adaptive(efforts=_E46, display=None),
|
|
97
|
+
# Legacy extended thinking: enabled + budget_tokens only.
|
|
98
|
+
"claude-haiku-4-5": _LEGACY,
|
|
99
|
+
"claude-sonnet-4-5": _LEGACY,
|
|
100
|
+
"claude-opus-4-5": _LEGACY,
|
|
101
|
+
"claude-opus-4-1": _LEGACY,
|
|
102
|
+
"claude-opus-4": _LEGACY,
|
|
103
|
+
"claude-sonnet-4": _LEGACY,
|
|
104
|
+
"claude-3-7-sonnet": _LEGACY,
|
|
105
|
+
# No extended thinking.
|
|
106
|
+
"claude-3-5-haiku": _NONE,
|
|
107
|
+
"claude-3-5-sonnet": _NONE,
|
|
108
|
+
"claude-3-haiku": _NONE,
|
|
109
|
+
"claude-3-opus": _NONE,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
UNKNOWN = _NONE # see module docstring: omit thinking for unknown ids
|
|
113
|
+
|
|
114
|
+
_PREFIX = re.compile(r"^(?:[a-z]+\.)?anthropic\.") # Bedrock (+ geo)
|
|
115
|
+
_SUFFIXES = (
|
|
116
|
+
re.compile(r"\[[^\]]*\]$"), # [1m]
|
|
117
|
+
re.compile(r"@.*$"), # Vertex @20251001
|
|
118
|
+
re.compile(r"-v\d+(?::\d+)?$"), # Bedrock -v1:0
|
|
119
|
+
re.compile(r"-\d{8}$"), # dated snapshot
|
|
120
|
+
re.compile(r"-latest$"),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def normalize_model_id(model: str) -> str:
|
|
125
|
+
m = (model or "").strip().lower()
|
|
126
|
+
m = m.rsplit("/", 1)[-1] # ARNs / inference-profile paths
|
|
127
|
+
m = _PREFIX.sub("", m)
|
|
128
|
+
for _ in range(2):
|
|
129
|
+
for rx in _SUFFIXES:
|
|
130
|
+
m = rx.sub("", m)
|
|
131
|
+
return m
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def capability_for(model: str) -> ModelThinking:
|
|
135
|
+
return CAPABILITIES.get(normalize_model_id(model), UNKNOWN)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def load_model_overrides(model: str) -> dict:
|
|
139
|
+
"""``models.<id>`` thinking/effort/thinking_budget from models.yaml
|
|
140
|
+
(user copy, else packaged). Matches the raw id first, then the
|
|
141
|
+
normalised one. Never raises."""
|
|
142
|
+
try:
|
|
143
|
+
import yaml
|
|
144
|
+
from hyperclaw.api_utils import find_config
|
|
145
|
+
p = find_config("models.yaml")
|
|
146
|
+
data = yaml.safe_load(p.read_text()) if p.exists() else {}
|
|
147
|
+
models = (data or {}).get("models") or {}
|
|
148
|
+
entry = models.get(model)
|
|
149
|
+
if entry is None:
|
|
150
|
+
norm = normalize_model_id(model)
|
|
151
|
+
entry = next((v for k, v in models.items()
|
|
152
|
+
if normalize_model_id(str(k)) == norm), None)
|
|
153
|
+
entry = entry or {}
|
|
154
|
+
return {k: entry[k] for k in ("thinking", "effort", "thinking_budget")
|
|
155
|
+
if k in entry and entry[k] is not None}
|
|
156
|
+
except Exception as e: # config problems must not break a chat turn
|
|
157
|
+
log.warning(f"models.yaml thinking overrides unreadable ({e})")
|
|
158
|
+
return {}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def thinking_params(model: str, max_tokens: int = 16000,
|
|
162
|
+
overrides: Optional[dict] = None,
|
|
163
|
+
use_config: bool = True) -> dict:
|
|
164
|
+
"""Request kwargs for ``model``: always ``max_tokens``; ``thinking`` and
|
|
165
|
+
``output_config`` only when the model takes them.
|
|
166
|
+
|
|
167
|
+
``overrides`` (thinking/effort/thinking_budget) wins over models.yaml,
|
|
168
|
+
which wins over the capability table."""
|
|
169
|
+
cap = capability_for(model)
|
|
170
|
+
ov = {}
|
|
171
|
+
if use_config:
|
|
172
|
+
ov.update(load_model_overrides(model))
|
|
173
|
+
ov.update(overrides or {})
|
|
174
|
+
|
|
175
|
+
mode = str(ov.get("thinking", cap.mode)).lower()
|
|
176
|
+
if mode in ("false", "off", "disabled"):
|
|
177
|
+
mode = "none"
|
|
178
|
+
if mode not in THINKING_MODES:
|
|
179
|
+
log.warning(f"thinking={mode!r} for {model} is not one of {THINKING_MODES}; "
|
|
180
|
+
f"using {cap.mode}")
|
|
181
|
+
mode = cap.mode
|
|
182
|
+
if mode == "enabled" and not cap.budget_tokens:
|
|
183
|
+
log.warning(f"{model} does not accept thinking.type 'enabled'; using adaptive")
|
|
184
|
+
mode = "adaptive"
|
|
185
|
+
if mode == "adaptive" and cap is UNKNOWN and "thinking" not in ov:
|
|
186
|
+
mode = "none"
|
|
187
|
+
|
|
188
|
+
out: dict = {"max_tokens": int(max_tokens)}
|
|
189
|
+
if mode == "adaptive":
|
|
190
|
+
thinking = {"type": "adaptive"}
|
|
191
|
+
if cap.display:
|
|
192
|
+
thinking["display"] = cap.display
|
|
193
|
+
out["thinking"] = thinking
|
|
194
|
+
effort = ov.get("effort", cap.effort)
|
|
195
|
+
if effort is not None:
|
|
196
|
+
effort = str(effort).lower()
|
|
197
|
+
allowed = cap.efforts or ALL_EFFORTS
|
|
198
|
+
if effort in allowed:
|
|
199
|
+
out["output_config"] = {"effort": effort}
|
|
200
|
+
else:
|
|
201
|
+
log.warning(f"effort={effort!r} not supported by {model}; omitted")
|
|
202
|
+
elif mode == "enabled":
|
|
203
|
+
budget = int(ov.get("thinking_budget", DEFAULT_THINKING_BUDGET))
|
|
204
|
+
budget = min(budget, int(max_tokens) - MIN_THINKING_BUDGET) # leave room to answer
|
|
205
|
+
if budget >= MIN_THINKING_BUDGET:
|
|
206
|
+
out["thinking"] = {"type": "enabled", "budget_tokens": budget}
|
|
207
|
+
return out
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def as_sdk_kwargs(params: dict, method) -> dict:
|
|
211
|
+
"""Adapt :func:`thinking_params` output to the installed SDK: an SDK too
|
|
212
|
+
old to know ``output_config`` (pyproject allows anthropic>=0.28) gets it
|
|
213
|
+
via ``extra_body`` so the wire request is identical."""
|
|
214
|
+
out = dict(params)
|
|
215
|
+
if "output_config" in out:
|
|
216
|
+
try:
|
|
217
|
+
import inspect
|
|
218
|
+
known = "output_config" in inspect.signature(method).parameters
|
|
219
|
+
except (TypeError, ValueError):
|
|
220
|
+
known = True
|
|
221
|
+
if not known:
|
|
222
|
+
extra = dict(out.pop("extra_body", None) or {})
|
|
223
|
+
extra["output_config"] = out.pop("output_config")
|
|
224
|
+
out["extra_body"] = extra
|
|
225
|
+
return out
|
hyperclaw/api_utils.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Helpers for reading Claude API responses and locating config files.
|
|
2
|
+
|
|
3
|
+
Adaptive-thinking models (the Claude 5 family, Opus/Sonnet 4.6 and later)
|
|
4
|
+
may return thinking blocks before — or instead of — text blocks, so
|
|
5
|
+
``response.content[0]`` is not guaranteed to be a TextBlock. Reading
|
|
6
|
+
``.content[0].text`` raises AttributeError on those responses. Always go
|
|
7
|
+
through :func:`extract_text` / :func:`extract_json` instead.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import shutil
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def extract_text(response, default: str = "") -> str:
|
|
18
|
+
"""Join all text blocks of a Messages API response, skipping thinking
|
|
19
|
+
and tool_use blocks. Returns ``default`` when no text block exists."""
|
|
20
|
+
blocks = getattr(response, "content", None) or []
|
|
21
|
+
parts = [b.text for b in blocks if getattr(b, "type", "") == "text" and getattr(b, "text", "")]
|
|
22
|
+
return "\n".join(parts).strip() or default
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def extract_json(response):
|
|
26
|
+
"""extract_text + json.loads, tolerating ```json fences around the body."""
|
|
27
|
+
text = extract_text(response)
|
|
28
|
+
m = re.search(r"```(?:json)?\s*(.*?)```", text, re.S)
|
|
29
|
+
if m:
|
|
30
|
+
text = m.group(1)
|
|
31
|
+
return json.loads(text)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def gmail_token_path() -> Path:
|
|
35
|
+
"""The Gmail OAuth token file the Gmail tools read.
|
|
36
|
+
|
|
37
|
+
Checks ``workspace/secrets/`` (the historical location) then ``secrets/``
|
|
38
|
+
(the directory onboarding creates), under ``$HYPERCLAW_ROOT`` and
|
|
39
|
+
``~/.hyperclaw``. Returns the first that exists, else the historical path.
|
|
40
|
+
"""
|
|
41
|
+
roots = [Path(os.environ.get("HYPERCLAW_ROOT", Path.home() / ".hyperclaw")),
|
|
42
|
+
Path.home() / ".hyperclaw"]
|
|
43
|
+
candidates = []
|
|
44
|
+
for root in roots:
|
|
45
|
+
for sub in ("workspace/secrets", "secrets"):
|
|
46
|
+
c = root / sub / "gmail_token.json"
|
|
47
|
+
if c not in candidates:
|
|
48
|
+
candidates.append(c)
|
|
49
|
+
for c in candidates:
|
|
50
|
+
if c.exists():
|
|
51
|
+
return Path(os.path.realpath(c))
|
|
52
|
+
return candidates[0]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def gmail_configured() -> bool:
|
|
56
|
+
"""True when the Gmail tools have credentials: a token file, or the
|
|
57
|
+
GMAIL_CLIENT_ID / GMAIL_CLIENT_SECRET / GMAIL_REFRESH_TOKEN env fallback."""
|
|
58
|
+
env = os.environ
|
|
59
|
+
return (gmail_token_path().exists() or bool(env.get("GMAIL_TOKEN"))
|
|
60
|
+
or all(env.get(k) for k in ("GMAIL_CLIENT_ID", "GMAIL_CLIENT_SECRET",
|
|
61
|
+
"GMAIL_REFRESH_TOKEN")))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def find_config(name: str) -> Path:
|
|
65
|
+
"""Locate a HyperClaw config file (e.g. ``agents.yaml``).
|
|
66
|
+
|
|
67
|
+
Order: the user's ``$HYPERCLAW_ROOT/config`` (seeded on first access),
|
|
68
|
+
then a development checkout's ``config/`` directory, then the copy
|
|
69
|
+
shipped inside the package. Always returns the user path so edits land
|
|
70
|
+
in one place; callers should still check ``.exists()``.
|
|
71
|
+
"""
|
|
72
|
+
root = Path(os.environ.get("HYPERCLAW_ROOT", Path.home() / ".hyperclaw"))
|
|
73
|
+
user = root / "config" / name
|
|
74
|
+
if user.exists():
|
|
75
|
+
return user
|
|
76
|
+
here = Path(__file__).resolve().parent
|
|
77
|
+
for src in (here.parent / "config" / name, here / "default_config" / name):
|
|
78
|
+
if src.exists():
|
|
79
|
+
try:
|
|
80
|
+
user.parent.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
shutil.copy(src, user)
|
|
82
|
+
return user
|
|
83
|
+
except OSError:
|
|
84
|
+
return src
|
|
85
|
+
return user
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Send / Edit / Cancel in Telegram (inline buttons).
|
|
3
|
+
|
|
4
|
+
Every item in the durable approval queue (tools/approval.py) is shown to the
|
|
5
|
+
owner chat with its exact content and three buttons. Only the owner can press
|
|
6
|
+
them: a button press from any other chat or user is refused and logged.
|
|
7
|
+
|
|
8
|
+
- Send -> the stored payload runs once, only if the hash on the button still
|
|
9
|
+
matches the stored payload.
|
|
10
|
+
- Edit -> the bot asks for the new content; the owner's next message in that
|
|
11
|
+
chat replaces it, and the new version is shown again with new
|
|
12
|
+
buttons (the old buttons stop working).
|
|
13
|
+
- Cancel -> nothing runs.
|
|
14
|
+
|
|
15
|
+
Cards go out with link previews off. Terminal-only actions (shell, desktop
|
|
16
|
+
control) cannot be approved from here.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import logging
|
|
22
|
+
from typing import Dict, List, Optional, Tuple
|
|
23
|
+
|
|
24
|
+
from hyperclaw.chat_scope import OWNER, owner_ids, scope_for
|
|
25
|
+
from hyperclaw.tools import approval as ap
|
|
26
|
+
|
|
27
|
+
log = logging.getLogger("hyperclaw.telegram.approval")
|
|
28
|
+
|
|
29
|
+
CB_PREFIX = "ap"
|
|
30
|
+
CHUNK = 3900
|
|
31
|
+
OPS = {"s": "send", "e": "edit", "c": "cancel"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def callback_data(op: str, item: ap.PendingAction) -> str:
|
|
35
|
+
return f"{CB_PREFIX}:{op}:{item.id}:{item.short_hash}" # < 64 bytes
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_callback(data: str) -> Optional[Tuple[str, str, str]]:
|
|
39
|
+
parts = str(data or "").split(":")
|
|
40
|
+
if len(parts) != 4 or parts[0] != CB_PREFIX or parts[1] not in OPS:
|
|
41
|
+
return None
|
|
42
|
+
return parts[1], parts[2], parts[3]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def keyboard(item: ap.PendingAction):
|
|
46
|
+
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
|
47
|
+
return InlineKeyboardMarkup([[
|
|
48
|
+
InlineKeyboardButton("Send", callback_data=callback_data("s", item)),
|
|
49
|
+
InlineKeyboardButton("Edit", callback_data=callback_data("e", item)),
|
|
50
|
+
InlineKeyboardButton("Cancel", callback_data=callback_data("c", item)),
|
|
51
|
+
]])
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def card_chunks(item: ap.PendingAction) -> List[str]:
|
|
55
|
+
"""The full card split into Telegram-sized parts (the exact content is
|
|
56
|
+
never cut; the last part carries the buttons)."""
|
|
57
|
+
text = ap.render(item)
|
|
58
|
+
if item.tool in ap._terminal_only():
|
|
59
|
+
text += "\n(This can only be approved in the HyperAgents terminal: /approvals.)"
|
|
60
|
+
return [text[i:i + CHUNK] for i in range(0, len(text), CHUNK)] or [text]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def actor_for(chat_id, user_id) -> ap.Actor:
|
|
64
|
+
"""Owner only when the chat is the owner chat AND the person pressing is
|
|
65
|
+
the owner (in a private chat the user id is the chat id)."""
|
|
66
|
+
chat_ok = scope_for("telegram", chat_id) == OWNER
|
|
67
|
+
user_ok = user_id is not None and (str(user_id) == str(chat_id)
|
|
68
|
+
or str(user_id) in owner_ids("telegram"))
|
|
69
|
+
role = "owner" if (chat_ok and user_ok) else "guest"
|
|
70
|
+
return ap.Actor(role, str(user_id), "telegram")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def push_pending(bot, queue: Optional[ap.ApprovalQueue] = None) -> int:
|
|
74
|
+
"""Show every not-yet-shown pending item to the owner chat(s)."""
|
|
75
|
+
q = queue or ap.get_queue()
|
|
76
|
+
owners = sorted(owner_ids("telegram"))
|
|
77
|
+
if not owners:
|
|
78
|
+
return 0
|
|
79
|
+
sent = 0
|
|
80
|
+
for item in q.pending(unnotified_only=True):
|
|
81
|
+
notes = []
|
|
82
|
+
for chat in owners:
|
|
83
|
+
try:
|
|
84
|
+
await show(bot, chat, item)
|
|
85
|
+
notes.append(f"telegram:{chat}")
|
|
86
|
+
except Exception as e:
|
|
87
|
+
log.warning("could not show approval %s to the owner chat: %s", item.id,
|
|
88
|
+
type(e).__name__)
|
|
89
|
+
if notes:
|
|
90
|
+
q.mark_notified(item.id, ",".join(notes))
|
|
91
|
+
sent += 1
|
|
92
|
+
return sent
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def show(bot, chat_id, item: ap.PendingAction) -> None:
|
|
96
|
+
from hyperclaw.tools.egress import TELEGRAM_NO_PREVIEW
|
|
97
|
+
from telegram import LinkPreviewOptions
|
|
98
|
+
no_preview = LinkPreviewOptions(is_disabled=TELEGRAM_NO_PREVIEW["link_preview_options"]
|
|
99
|
+
["is_disabled"])
|
|
100
|
+
chunks = card_chunks(item)
|
|
101
|
+
for i, part in enumerate(chunks):
|
|
102
|
+
last = i == len(chunks) - 1
|
|
103
|
+
await bot.send_message(chat_id=int(chat_id), text=part,
|
|
104
|
+
link_preview_options=no_preview,
|
|
105
|
+
reply_markup=keyboard(item) if last else None)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class TelegramApprovals:
|
|
109
|
+
"""Button presses and the Edit conversation for one bot process."""
|
|
110
|
+
|
|
111
|
+
def __init__(self, registry=None, queue: Optional[ap.ApprovalQueue] = None):
|
|
112
|
+
self.registry = registry
|
|
113
|
+
self.queue = queue
|
|
114
|
+
self.awaiting_edit: Dict[int, str] = {} # chat_id -> action id
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def q(self) -> ap.ApprovalQueue:
|
|
118
|
+
return self.queue or ap.get_queue()
|
|
119
|
+
|
|
120
|
+
async def on_callback(self, update, context) -> None:
|
|
121
|
+
query = update.callback_query
|
|
122
|
+
parsed = parse_callback(getattr(query, "data", ""))
|
|
123
|
+
if parsed is None:
|
|
124
|
+
return
|
|
125
|
+
op, aid, h = parsed
|
|
126
|
+
chat_id = query.message.chat.id if query.message else None
|
|
127
|
+
user_id = query.from_user.id if query.from_user else None
|
|
128
|
+
actor = actor_for(chat_id, user_id)
|
|
129
|
+
if not actor.is_owner:
|
|
130
|
+
log.warning("approval button pressed by a non-owner (chat %s); refused", chat_id)
|
|
131
|
+
# Records the refusal in the append-only events (raises ApprovalError).
|
|
132
|
+
try:
|
|
133
|
+
self.q.cancel(aid, actor)
|
|
134
|
+
except ap.ApprovalError:
|
|
135
|
+
pass
|
|
136
|
+
await query.answer("Only the owner can approve this.", show_alert=True)
|
|
137
|
+
return
|
|
138
|
+
if op == "s":
|
|
139
|
+
await query.answer("Sending…")
|
|
140
|
+
loop = asyncio.get_running_loop()
|
|
141
|
+
env = await loop.run_in_executor(
|
|
142
|
+
None, lambda: ap.send(aid, h, actor, registry=self.registry, queue=self.q))
|
|
143
|
+
await self._close_card(query)
|
|
144
|
+
await query.message.reply_text(_outcome("Sent" if env.ok else "Not sent", env))
|
|
145
|
+
elif op == "c":
|
|
146
|
+
env = ap.cancel(aid, actor, queue=self.q)
|
|
147
|
+
await query.answer("Cancelled" if "Cancelled" in env.message else "Not cancelled")
|
|
148
|
+
await self._close_card(query)
|
|
149
|
+
await query.message.reply_text(env.message)
|
|
150
|
+
else:
|
|
151
|
+
item = self.q.get(aid)
|
|
152
|
+
if item is None or item.status != ap.PENDING or not item.payload_hash.startswith(h):
|
|
153
|
+
await query.answer("This is no longer pending.", show_alert=True)
|
|
154
|
+
return
|
|
155
|
+
if item.tool in ap._terminal_only():
|
|
156
|
+
await query.answer("Edit this in the HyperAgents terminal.", show_alert=True)
|
|
157
|
+
return
|
|
158
|
+
self.awaiting_edit[int(chat_id)] = aid
|
|
159
|
+
fld = ap.EDIT_FIELD.get(item.tool)
|
|
160
|
+
await query.answer()
|
|
161
|
+
await query.message.reply_text(
|
|
162
|
+
f"Send me the new {fld} for [{aid}] (or a JSON object with all the arguments). "
|
|
163
|
+
f"I'll show it again before anything is sent." if fld else
|
|
164
|
+
f"Send me the new arguments for [{aid}] as a JSON object. I'll show it again "
|
|
165
|
+
f"before anything is sent.")
|
|
166
|
+
|
|
167
|
+
async def on_owner_text(self, update, context) -> bool:
|
|
168
|
+
"""The owner's reply after Edit. True when the message was consumed."""
|
|
169
|
+
chat_id = update.effective_chat.id
|
|
170
|
+
aid = self.awaiting_edit.get(int(chat_id))
|
|
171
|
+
if not aid:
|
|
172
|
+
return False
|
|
173
|
+
user_id = update.effective_user.id if update.effective_user else None
|
|
174
|
+
actor = actor_for(chat_id, user_id)
|
|
175
|
+
if not actor.is_owner:
|
|
176
|
+
return False
|
|
177
|
+
self.awaiting_edit.pop(int(chat_id), None)
|
|
178
|
+
try:
|
|
179
|
+
new_item = ap.edit(aid, update.message.text, actor, registry=self.registry,
|
|
180
|
+
queue=self.q)
|
|
181
|
+
except ap.ApprovalError as e:
|
|
182
|
+
await update.message.reply_text(f"Edit not applied: {e}")
|
|
183
|
+
return True
|
|
184
|
+
await show(context.bot, chat_id, new_item)
|
|
185
|
+
self.q.mark_notified(new_item.id, f"telegram:{chat_id}")
|
|
186
|
+
return True
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
async def _close_card(query) -> None:
|
|
190
|
+
try:
|
|
191
|
+
await query.edit_message_reply_markup(reply_markup=None)
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _outcome(prefix: str, env) -> str:
|
|
197
|
+
rid = f" (receipt {env.receipt_id})" if getattr(env, "receipt_id", None) else ""
|
|
198
|
+
return f"{prefix}: {env.status}. {env.message[:3000]}{rid}"
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
async def pending_loop(bot, interval: float = 5.0) -> None:
|
|
202
|
+
"""Background: deliver cards queued by any surface (bridge, sub-agents,
|
|
203
|
+
dashboard) to the owner chat."""
|
|
204
|
+
while True:
|
|
205
|
+
try:
|
|
206
|
+
await push_pending(bot)
|
|
207
|
+
except Exception:
|
|
208
|
+
log.warning("approval push failed", exc_info=True)
|
|
209
|
+
await asyncio.sleep(interval)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Send / Edit / Cancel in the HyperAgents terminal.
|
|
3
|
+
|
|
4
|
+
When a tool call in the terminal is queued for approval, the owner sees the
|
|
5
|
+
exact content right away and answers s (send), e (edit) or c (cancel). Edit
|
|
6
|
+
asks for the new content and shows it again. The terminal is the owner's own
|
|
7
|
+
surface, so its answers are owner decisions; nothing here runs without one.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Callable, Optional
|
|
13
|
+
|
|
14
|
+
from hyperclaw.tools import approval as ap
|
|
15
|
+
from hyperclaw.tools.envelope import Status, make
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def interactive() -> bool:
|
|
19
|
+
try:
|
|
20
|
+
return sys.stdin.isatty() and sys.stdout.isatty()
|
|
21
|
+
except (AttributeError, ValueError):
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def prompt(item: ap.PendingAction, *, input_fn: Optional[Callable[[str], str]] = None,
|
|
26
|
+
print_fn: Optional[Callable[[str], None]] = None, registry=None,
|
|
27
|
+
queue: Optional[ap.ApprovalQueue] = None, ledger=None):
|
|
28
|
+
"""Ask the owner about one queued item. Returns the envelope the model
|
|
29
|
+
sees: the tool's real result after Send, or a 'nothing was done' result."""
|
|
30
|
+
import builtins
|
|
31
|
+
input_fn = input_fn or builtins.input
|
|
32
|
+
print_fn = print_fn or builtins.print
|
|
33
|
+
q = queue or ap.get_queue()
|
|
34
|
+
while True:
|
|
35
|
+
try:
|
|
36
|
+
q.mark_notified(item.id, "tui") # shown here; no second card elsewhere
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
print_fn("\n" + ap.render(item))
|
|
40
|
+
try:
|
|
41
|
+
ans = input_fn("Send, edit or cancel? [s/e/c] ").strip().lower()
|
|
42
|
+
except (EOFError, KeyboardInterrupt):
|
|
43
|
+
ans = "c"
|
|
44
|
+
if ans in ("s", "send"):
|
|
45
|
+
return ap.send(item.id, item.payload_hash, ap.TERMINAL_OWNER, registry=registry,
|
|
46
|
+
queue=q, ledger=ledger)
|
|
47
|
+
if ans in ("c", "cancel", "n", "no"):
|
|
48
|
+
return ap.cancel(item.id, ap.TERMINAL_OWNER, queue=q)
|
|
49
|
+
if ans in ("e", "edit"):
|
|
50
|
+
fld = ap.EDIT_FIELD.get(item.tool)
|
|
51
|
+
hint = (f"New {fld} (or a JSON object of all arguments)" if fld
|
|
52
|
+
else "New arguments as a JSON object")
|
|
53
|
+
try:
|
|
54
|
+
new = input_fn(f"{hint}: ")
|
|
55
|
+
except (EOFError, KeyboardInterrupt):
|
|
56
|
+
continue
|
|
57
|
+
try:
|
|
58
|
+
item = ap.edit(item.id, new, ap.TERMINAL_OWNER, registry=registry, queue=q)
|
|
59
|
+
except ap.ApprovalError as e:
|
|
60
|
+
print_fn(f"Edit not applied: {e}")
|
|
61
|
+
fresh = q.get(item.id)
|
|
62
|
+
if fresh is None or fresh.status != ap.PENDING:
|
|
63
|
+
return make(Status.PERMISSION_DENIED, f"Not sent: {e}")
|
|
64
|
+
continue
|
|
65
|
+
print_fn("Please answer s, e or c.")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def review_pending(*, input_fn=None, print_fn=None, registry=None) -> int:
|
|
69
|
+
"""/approvals: walk every pending item (from any surface) with the prompt.
|
|
70
|
+
Terminal-only tools queued elsewhere can only be approved here."""
|
|
71
|
+
import builtins
|
|
72
|
+
print_fn = print_fn or builtins.print
|
|
73
|
+
items = ap.get_queue().pending()
|
|
74
|
+
if not items:
|
|
75
|
+
print_fn("Nothing is waiting for approval.")
|
|
76
|
+
return 0
|
|
77
|
+
for item in items:
|
|
78
|
+
env = prompt(item, input_fn=input_fn, print_fn=print_fn, registry=registry)
|
|
79
|
+
print_fn(f"[{item.id}] {env.status}: {env.message[:300]}")
|
|
80
|
+
return len(items)
|