hermes-agent 0.10.0__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.
- acp_adapter/__init__.py +1 -0
- acp_adapter/__main__.py +5 -0
- acp_adapter/auth.py +24 -0
- acp_adapter/entry.py +85 -0
- acp_adapter/events.py +175 -0
- acp_adapter/permissions.py +77 -0
- acp_adapter/server.py +728 -0
- acp_adapter/session.py +475 -0
- acp_adapter/tools.py +214 -0
- agent/__init__.py +6 -0
- agent/anthropic_adapter.py +1520 -0
- agent/auxiliary_client.py +2716 -0
- agent/bedrock_adapter.py +1098 -0
- agent/context_compressor.py +1163 -0
- agent/context_engine.py +184 -0
- agent/context_references.py +520 -0
- agent/copilot_acp_client.py +586 -0
- agent/credential_pool.py +1418 -0
- agent/display.py +996 -0
- agent/error_classifier.py +829 -0
- agent/gemini_cloudcode_adapter.py +764 -0
- agent/google_code_assist.py +417 -0
- agent/google_oauth.py +1048 -0
- agent/insights.py +768 -0
- agent/manual_compression_feedback.py +49 -0
- agent/memory_manager.py +373 -0
- agent/memory_provider.py +231 -0
- agent/model_metadata.py +1116 -0
- agent/models_dev.py +586 -0
- agent/nous_rate_guard.py +182 -0
- agent/prompt_builder.py +1045 -0
- agent/prompt_caching.py +72 -0
- agent/rate_limit_tracker.py +246 -0
- agent/redact.py +198 -0
- agent/retry_utils.py +57 -0
- agent/skill_commands.py +377 -0
- agent/skill_utils.py +465 -0
- agent/smart_model_routing.py +195 -0
- agent/subdirectory_hints.py +224 -0
- agent/title_generator.py +125 -0
- agent/trajectory.py +56 -0
- agent/usage_pricing.py +687 -0
- batch_runner.py +1290 -0
- cli.py +10275 -0
- cron/__init__.py +42 -0
- cron/jobs.py +768 -0
- cron/scheduler.py +1009 -0
- gateway/__init__.py +35 -0
- gateway/builtin_hooks/__init__.py +1 -0
- gateway/builtin_hooks/boot_md.py +85 -0
- gateway/channel_directory.py +276 -0
- gateway/config.py +1178 -0
- gateway/delivery.py +256 -0
- gateway/display_config.py +194 -0
- gateway/hooks.py +170 -0
- gateway/mirror.py +132 -0
- gateway/pairing.py +309 -0
- gateway/platforms/__init__.py +19 -0
- gateway/platforms/api_server.py +2436 -0
- gateway/platforms/base.py +2164 -0
- gateway/platforms/bluebubbles.py +918 -0
- gateway/platforms/dingtalk.py +347 -0
- gateway/platforms/discord.py +3191 -0
- gateway/platforms/email.py +625 -0
- gateway/platforms/feishu.py +4131 -0
- gateway/platforms/helpers.py +264 -0
- gateway/platforms/homeassistant.py +449 -0
- gateway/platforms/matrix.py +2216 -0
- gateway/platforms/mattermost.py +740 -0
- gateway/platforms/qqbot.py +1960 -0
- gateway/platforms/signal.py +825 -0
- gateway/platforms/slack.py +1695 -0
- gateway/platforms/sms.py +373 -0
- gateway/platforms/telegram.py +2914 -0
- gateway/platforms/telegram_network.py +246 -0
- gateway/platforms/webhook.py +672 -0
- gateway/platforms/wecom.py +1430 -0
- gateway/platforms/wecom_callback.py +401 -0
- gateway/platforms/wecom_crypto.py +142 -0
- gateway/platforms/weixin.py +1829 -0
- gateway/platforms/whatsapp.py +989 -0
- gateway/restart.py +20 -0
- gateway/run.py +9892 -0
- gateway/session.py +1090 -0
- gateway/session_context.py +145 -0
- gateway/status.py +455 -0
- gateway/sticker_cache.py +111 -0
- gateway/stream_consumer.py +761 -0
- hermes_agent-0.10.0.dist-info/METADATA +295 -0
- hermes_agent-0.10.0.dist-info/RECORD +243 -0
- hermes_agent-0.10.0.dist-info/WHEEL +5 -0
- hermes_agent-0.10.0.dist-info/entry_points.txt +4 -0
- hermes_agent-0.10.0.dist-info/licenses/LICENSE +21 -0
- hermes_agent-0.10.0.dist-info/top_level.txt +20 -0
- hermes_cli/__init__.py +15 -0
- hermes_cli/auth.py +3429 -0
- hermes_cli/auth_commands.py +587 -0
- hermes_cli/backup.py +655 -0
- hermes_cli/banner.py +535 -0
- hermes_cli/callbacks.py +242 -0
- hermes_cli/claw.py +734 -0
- hermes_cli/cli_output.py +78 -0
- hermes_cli/clipboard.py +432 -0
- hermes_cli/codex_models.py +176 -0
- hermes_cli/colors.py +38 -0
- hermes_cli/commands.py +1234 -0
- hermes_cli/completion.py +315 -0
- hermes_cli/config.py +3619 -0
- hermes_cli/copilot_auth.py +299 -0
- hermes_cli/cron.py +290 -0
- hermes_cli/curses_ui.py +466 -0
- hermes_cli/debug.py +477 -0
- hermes_cli/default_soul.py +11 -0
- hermes_cli/doctor.py +1149 -0
- hermes_cli/dump.py +345 -0
- hermes_cli/env_loader.py +123 -0
- hermes_cli/gateway.py +3161 -0
- hermes_cli/logs.py +390 -0
- hermes_cli/main.py +6552 -0
- hermes_cli/mcp_config.py +777 -0
- hermes_cli/memory_setup.py +457 -0
- hermes_cli/model_normalize.py +407 -0
- hermes_cli/model_switch.py +1125 -0
- hermes_cli/models.py +2173 -0
- hermes_cli/nous_subscription.py +778 -0
- hermes_cli/pairing.py +97 -0
- hermes_cli/platforms.py +47 -0
- hermes_cli/plugins.py +843 -0
- hermes_cli/plugins_cmd.py +1128 -0
- hermes_cli/profiles.py +1094 -0
- hermes_cli/providers.py +583 -0
- hermes_cli/runtime_provider.py +996 -0
- hermes_cli/setup.py +3235 -0
- hermes_cli/skills_config.py +177 -0
- hermes_cli/skills_hub.py +1300 -0
- hermes_cli/skin_engine.py +818 -0
- hermes_cli/status.py +488 -0
- hermes_cli/tips.py +349 -0
- hermes_cli/tools_config.py +1868 -0
- hermes_cli/uninstall.py +326 -0
- hermes_cli/web_server.py +2350 -0
- hermes_cli/webhook.py +259 -0
- hermes_constants.py +294 -0
- hermes_logging.py +390 -0
- hermes_state.py +1238 -0
- hermes_time.py +104 -0
- model_tools.py +562 -0
- plugins/__init__.py +1 -0
- plugins/context_engine/__init__.py +219 -0
- plugins/example-dashboard/dashboard/plugin_api.py +14 -0
- plugins/memory/__init__.py +406 -0
- plugins/memory/byterover/__init__.py +383 -0
- plugins/memory/hindsight/__init__.py +883 -0
- plugins/memory/holographic/__init__.py +407 -0
- plugins/memory/holographic/holographic.py +203 -0
- plugins/memory/holographic/retrieval.py +593 -0
- plugins/memory/holographic/store.py +574 -0
- plugins/memory/honcho/__init__.py +1054 -0
- plugins/memory/honcho/cli.py +1398 -0
- plugins/memory/honcho/client.py +676 -0
- plugins/memory/honcho/session.py +1255 -0
- plugins/memory/mem0/__init__.py +373 -0
- plugins/memory/openviking/__init__.py +674 -0
- plugins/memory/retaindb/__init__.py +766 -0
- plugins/memory/supermemory/__init__.py +791 -0
- rl_cli.py +446 -0
- run_agent.py +11603 -0
- tools/__init__.py +25 -0
- tools/ansi_strip.py +44 -0
- tools/approval.py +957 -0
- tools/binary_extensions.py +42 -0
- tools/browser_camofox.py +600 -0
- tools/browser_camofox_state.py +47 -0
- tools/browser_providers/__init__.py +10 -0
- tools/browser_providers/base.py +59 -0
- tools/browser_providers/browser_use.py +215 -0
- tools/browser_providers/browserbase.py +217 -0
- tools/browser_providers/firecrawl.py +107 -0
- tools/browser_tool.py +2418 -0
- tools/budget_config.py +52 -0
- tools/checkpoint_manager.py +654 -0
- tools/clarify_tool.py +141 -0
- tools/code_execution_tool.py +1416 -0
- tools/credential_files.py +407 -0
- tools/cronjob_tools.py +510 -0
- tools/debug_helpers.py +105 -0
- tools/delegate_tool.py +1143 -0
- tools/env_passthrough.py +101 -0
- tools/environments/__init__.py +13 -0
- tools/environments/base.py +599 -0
- tools/environments/daytona.py +259 -0
- tools/environments/docker.py +578 -0
- tools/environments/file_sync.py +393 -0
- tools/environments/local.py +314 -0
- tools/environments/managed_modal.py +282 -0
- tools/environments/modal.py +460 -0
- tools/environments/modal_utils.py +199 -0
- tools/environments/singularity.py +262 -0
- tools/environments/ssh.py +275 -0
- tools/file_operations.py +1216 -0
- tools/file_tools.py +799 -0
- tools/fuzzy_match.py +566 -0
- tools/homeassistant_tool.py +513 -0
- tools/image_generation_tool.py +837 -0
- tools/interrupt.py +76 -0
- tools/managed_tool_gateway.py +167 -0
- tools/mcp_oauth.py +526 -0
- tools/mcp_oauth_manager.py +413 -0
- tools/mcp_tool.py +2599 -0
- tools/memory_tool.py +584 -0
- tools/mixture_of_agents_tool.py +539 -0
- tools/neutts_synth.py +104 -0
- tools/openrouter_client.py +33 -0
- tools/osv_check.py +155 -0
- tools/patch_parser.py +580 -0
- tools/path_security.py +43 -0
- tools/process_registry.py +1184 -0
- tools/registry.py +482 -0
- tools/rl_training_tool.py +1396 -0
- tools/send_message_tool.py +1304 -0
- tools/session_search_tool.py +562 -0
- tools/skill_manager_tool.py +789 -0
- tools/skills_guard.py +928 -0
- tools/skills_hub.py +3053 -0
- tools/skills_sync.py +414 -0
- tools/skills_tool.py +1420 -0
- tools/terminal_tool.py +1756 -0
- tools/tirith_security.py +684 -0
- tools/todo_tool.py +277 -0
- tools/tool_backend_helpers.py +121 -0
- tools/tool_result_storage.py +226 -0
- tools/transcription_tools.py +677 -0
- tools/tts_tool.py +1334 -0
- tools/url_safety.py +97 -0
- tools/vision_tools.py +789 -0
- tools/voice_mode.py +1017 -0
- tools/web_tools.py +2101 -0
- tools/website_policy.py +282 -0
- tools/xai_http.py +12 -0
- toolset_distributions.py +364 -0
- toolsets.py +702 -0
- trajectory_compressor.py +1462 -0
- utils.py +196 -0
acp_adapter/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""ACP (Agent Communication Protocol) adapter for hermes-agent."""
|
acp_adapter/__main__.py
ADDED
acp_adapter/auth.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""ACP auth helpers — detect the currently configured Hermes provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def detect_provider() -> Optional[str]:
|
|
9
|
+
"""Resolve the active Hermes runtime provider, or None if unavailable."""
|
|
10
|
+
try:
|
|
11
|
+
from hermes_cli.runtime_provider import resolve_runtime_provider
|
|
12
|
+
runtime = resolve_runtime_provider()
|
|
13
|
+
api_key = runtime.get("api_key")
|
|
14
|
+
provider = runtime.get("provider")
|
|
15
|
+
if isinstance(api_key, str) and api_key.strip() and isinstance(provider, str) and provider.strip():
|
|
16
|
+
return provider.strip().lower()
|
|
17
|
+
except Exception:
|
|
18
|
+
return None
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def has_provider() -> bool:
|
|
23
|
+
"""Return True if Hermes can resolve any runtime provider credentials."""
|
|
24
|
+
return detect_provider() is not None
|
acp_adapter/entry.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""CLI entry point for the hermes-agent ACP adapter.
|
|
2
|
+
|
|
3
|
+
Loads environment variables from ``~/.hermes/.env``, configures logging
|
|
4
|
+
to write to stderr (so stdout is reserved for ACP JSON-RPC transport),
|
|
5
|
+
and starts the ACP agent server.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
python -m acp_adapter.entry
|
|
10
|
+
# or
|
|
11
|
+
hermes acp
|
|
12
|
+
# or
|
|
13
|
+
hermes-acp
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import logging
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from hermes_constants import get_hermes_home
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _setup_logging() -> None:
|
|
24
|
+
"""Route all logging to stderr so stdout stays clean for ACP stdio."""
|
|
25
|
+
handler = logging.StreamHandler(sys.stderr)
|
|
26
|
+
handler.setFormatter(
|
|
27
|
+
logging.Formatter(
|
|
28
|
+
"%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
29
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
root = logging.getLogger()
|
|
33
|
+
root.handlers.clear()
|
|
34
|
+
root.addHandler(handler)
|
|
35
|
+
root.setLevel(logging.INFO)
|
|
36
|
+
|
|
37
|
+
# Quiet down noisy libraries
|
|
38
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
39
|
+
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
|
40
|
+
logging.getLogger("openai").setLevel(logging.WARNING)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _load_env() -> None:
|
|
44
|
+
"""Load .env from HERMES_HOME (default ``~/.hermes``)."""
|
|
45
|
+
from hermes_cli.env_loader import load_hermes_dotenv
|
|
46
|
+
|
|
47
|
+
hermes_home = get_hermes_home()
|
|
48
|
+
loaded = load_hermes_dotenv(hermes_home=hermes_home)
|
|
49
|
+
if loaded:
|
|
50
|
+
for env_file in loaded:
|
|
51
|
+
logging.getLogger(__name__).info("Loaded env from %s", env_file)
|
|
52
|
+
else:
|
|
53
|
+
logging.getLogger(__name__).info(
|
|
54
|
+
"No .env found at %s, using system env", hermes_home / ".env"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def main() -> None:
|
|
59
|
+
"""Entry point: load env, configure logging, run the ACP agent."""
|
|
60
|
+
_setup_logging()
|
|
61
|
+
_load_env()
|
|
62
|
+
|
|
63
|
+
logger = logging.getLogger(__name__)
|
|
64
|
+
logger.info("Starting hermes-agent ACP adapter")
|
|
65
|
+
|
|
66
|
+
# Ensure the project root is on sys.path so ``from run_agent import AIAgent`` works
|
|
67
|
+
project_root = str(Path(__file__).resolve().parent.parent)
|
|
68
|
+
if project_root not in sys.path:
|
|
69
|
+
sys.path.insert(0, project_root)
|
|
70
|
+
|
|
71
|
+
import acp
|
|
72
|
+
from .server import HermesACPAgent
|
|
73
|
+
|
|
74
|
+
agent = HermesACPAgent()
|
|
75
|
+
try:
|
|
76
|
+
asyncio.run(acp.run_agent(agent, use_unstable_protocol=True))
|
|
77
|
+
except KeyboardInterrupt:
|
|
78
|
+
logger.info("Shutting down (KeyboardInterrupt)")
|
|
79
|
+
except Exception:
|
|
80
|
+
logger.exception("ACP agent crashed")
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
main()
|
acp_adapter/events.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Callback factories for bridging AIAgent events to ACP notifications.
|
|
2
|
+
|
|
3
|
+
Each factory returns a callable with the signature that AIAgent expects
|
|
4
|
+
for its callbacks. Internally, the callbacks push ACP session updates
|
|
5
|
+
to the client via ``conn.session_update()`` using
|
|
6
|
+
``asyncio.run_coroutine_threadsafe()`` (since AIAgent runs in a worker
|
|
7
|
+
thread while the event loop lives on the main thread).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
from collections import deque
|
|
14
|
+
from typing import Any, Callable, Deque, Dict
|
|
15
|
+
|
|
16
|
+
import acp
|
|
17
|
+
|
|
18
|
+
from .tools import (
|
|
19
|
+
build_tool_complete,
|
|
20
|
+
build_tool_start,
|
|
21
|
+
make_tool_call_id,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _send_update(
|
|
28
|
+
conn: acp.Client,
|
|
29
|
+
session_id: str,
|
|
30
|
+
loop: asyncio.AbstractEventLoop,
|
|
31
|
+
update: Any,
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Fire-and-forget an ACP session update from a worker thread."""
|
|
34
|
+
try:
|
|
35
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
36
|
+
conn.session_update(session_id, update), loop
|
|
37
|
+
)
|
|
38
|
+
future.result(timeout=5)
|
|
39
|
+
except Exception:
|
|
40
|
+
logger.debug("Failed to send ACP update", exc_info=True)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ------------------------------------------------------------------
|
|
44
|
+
# Tool progress callback
|
|
45
|
+
# ------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
def make_tool_progress_cb(
|
|
48
|
+
conn: acp.Client,
|
|
49
|
+
session_id: str,
|
|
50
|
+
loop: asyncio.AbstractEventLoop,
|
|
51
|
+
tool_call_ids: Dict[str, Deque[str]],
|
|
52
|
+
) -> Callable:
|
|
53
|
+
"""Create a ``tool_progress_callback`` for AIAgent.
|
|
54
|
+
|
|
55
|
+
Signature expected by AIAgent::
|
|
56
|
+
|
|
57
|
+
tool_progress_callback(event_type: str, name: str, preview: str, args: dict, **kwargs)
|
|
58
|
+
|
|
59
|
+
Emits ``ToolCallStart`` for ``tool.started`` events and tracks IDs in a FIFO
|
|
60
|
+
queue per tool name so duplicate/parallel same-name calls still complete
|
|
61
|
+
against the correct ACP tool call. Other event types (``tool.completed``,
|
|
62
|
+
``reasoning.available``) are silently ignored.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def _tool_progress(event_type: str, name: str = None, preview: str = None, args: Any = None, **kwargs) -> None:
|
|
66
|
+
# Only emit ACP ToolCallStart for tool.started; ignore other event types
|
|
67
|
+
if event_type != "tool.started":
|
|
68
|
+
return
|
|
69
|
+
if isinstance(args, str):
|
|
70
|
+
try:
|
|
71
|
+
args = json.loads(args)
|
|
72
|
+
except (json.JSONDecodeError, TypeError):
|
|
73
|
+
args = {"raw": args}
|
|
74
|
+
if not isinstance(args, dict):
|
|
75
|
+
args = {}
|
|
76
|
+
|
|
77
|
+
tc_id = make_tool_call_id()
|
|
78
|
+
queue = tool_call_ids.get(name)
|
|
79
|
+
if queue is None:
|
|
80
|
+
queue = deque()
|
|
81
|
+
tool_call_ids[name] = queue
|
|
82
|
+
elif isinstance(queue, str):
|
|
83
|
+
queue = deque([queue])
|
|
84
|
+
tool_call_ids[name] = queue
|
|
85
|
+
queue.append(tc_id)
|
|
86
|
+
|
|
87
|
+
update = build_tool_start(tc_id, name, args)
|
|
88
|
+
_send_update(conn, session_id, loop, update)
|
|
89
|
+
|
|
90
|
+
return _tool_progress
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ------------------------------------------------------------------
|
|
94
|
+
# Thinking callback
|
|
95
|
+
# ------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
def make_thinking_cb(
|
|
98
|
+
conn: acp.Client,
|
|
99
|
+
session_id: str,
|
|
100
|
+
loop: asyncio.AbstractEventLoop,
|
|
101
|
+
) -> Callable:
|
|
102
|
+
"""Create a ``thinking_callback`` for AIAgent."""
|
|
103
|
+
|
|
104
|
+
def _thinking(text: str) -> None:
|
|
105
|
+
if not text:
|
|
106
|
+
return
|
|
107
|
+
update = acp.update_agent_thought_text(text)
|
|
108
|
+
_send_update(conn, session_id, loop, update)
|
|
109
|
+
|
|
110
|
+
return _thinking
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ------------------------------------------------------------------
|
|
114
|
+
# Step callback
|
|
115
|
+
# ------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
def make_step_cb(
|
|
118
|
+
conn: acp.Client,
|
|
119
|
+
session_id: str,
|
|
120
|
+
loop: asyncio.AbstractEventLoop,
|
|
121
|
+
tool_call_ids: Dict[str, Deque[str]],
|
|
122
|
+
) -> Callable:
|
|
123
|
+
"""Create a ``step_callback`` for AIAgent.
|
|
124
|
+
|
|
125
|
+
Signature expected by AIAgent::
|
|
126
|
+
|
|
127
|
+
step_callback(api_call_count: int, prev_tools: list)
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def _step(api_call_count: int, prev_tools: Any = None) -> None:
|
|
131
|
+
if prev_tools and isinstance(prev_tools, list):
|
|
132
|
+
for tool_info in prev_tools:
|
|
133
|
+
tool_name = None
|
|
134
|
+
result = None
|
|
135
|
+
|
|
136
|
+
if isinstance(tool_info, dict):
|
|
137
|
+
tool_name = tool_info.get("name") or tool_info.get("function_name")
|
|
138
|
+
result = tool_info.get("result") or tool_info.get("output")
|
|
139
|
+
elif isinstance(tool_info, str):
|
|
140
|
+
tool_name = tool_info
|
|
141
|
+
|
|
142
|
+
queue = tool_call_ids.get(tool_name or "")
|
|
143
|
+
if isinstance(queue, str):
|
|
144
|
+
queue = deque([queue])
|
|
145
|
+
tool_call_ids[tool_name] = queue
|
|
146
|
+
if tool_name and queue:
|
|
147
|
+
tc_id = queue.popleft()
|
|
148
|
+
update = build_tool_complete(
|
|
149
|
+
tc_id, tool_name, result=str(result) if result is not None else None
|
|
150
|
+
)
|
|
151
|
+
_send_update(conn, session_id, loop, update)
|
|
152
|
+
if not queue:
|
|
153
|
+
tool_call_ids.pop(tool_name, None)
|
|
154
|
+
|
|
155
|
+
return _step
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ------------------------------------------------------------------
|
|
159
|
+
# Agent message callback
|
|
160
|
+
# ------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
def make_message_cb(
|
|
163
|
+
conn: acp.Client,
|
|
164
|
+
session_id: str,
|
|
165
|
+
loop: asyncio.AbstractEventLoop,
|
|
166
|
+
) -> Callable:
|
|
167
|
+
"""Create a callback that streams agent response text to the editor."""
|
|
168
|
+
|
|
169
|
+
def _message(text: str) -> None:
|
|
170
|
+
if not text:
|
|
171
|
+
return
|
|
172
|
+
update = acp.update_agent_message_text(text)
|
|
173
|
+
_send_update(conn, session_id, loop, update)
|
|
174
|
+
|
|
175
|
+
return _message
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""ACP permission bridging — maps ACP approval requests to hermes approval callbacks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from concurrent.futures import TimeoutError as FutureTimeout
|
|
8
|
+
from typing import Callable
|
|
9
|
+
|
|
10
|
+
from acp.schema import (
|
|
11
|
+
AllowedOutcome,
|
|
12
|
+
PermissionOption,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# Maps ACP PermissionOptionKind -> hermes approval result strings
|
|
18
|
+
_KIND_TO_HERMES = {
|
|
19
|
+
"allow_once": "once",
|
|
20
|
+
"allow_always": "always",
|
|
21
|
+
"reject_once": "deny",
|
|
22
|
+
"reject_always": "deny",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def make_approval_callback(
|
|
27
|
+
request_permission_fn: Callable,
|
|
28
|
+
loop: asyncio.AbstractEventLoop,
|
|
29
|
+
session_id: str,
|
|
30
|
+
timeout: float = 60.0,
|
|
31
|
+
) -> Callable[[str, str], str]:
|
|
32
|
+
"""
|
|
33
|
+
Return a hermes-compatible ``approval_callback(command, description) -> str``
|
|
34
|
+
that bridges to the ACP client's ``request_permission`` call.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
request_permission_fn: The ACP connection's ``request_permission`` coroutine.
|
|
38
|
+
loop: The event loop on which the ACP connection lives.
|
|
39
|
+
session_id: Current ACP session id.
|
|
40
|
+
timeout: Seconds to wait for a response before auto-denying.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def _callback(command: str, description: str) -> str:
|
|
44
|
+
options = [
|
|
45
|
+
PermissionOption(option_id="allow_once", kind="allow_once", name="Allow once"),
|
|
46
|
+
PermissionOption(option_id="allow_always", kind="allow_always", name="Allow always"),
|
|
47
|
+
PermissionOption(option_id="deny", kind="reject_once", name="Deny"),
|
|
48
|
+
]
|
|
49
|
+
import acp as _acp
|
|
50
|
+
|
|
51
|
+
tool_call = _acp.start_tool_call("perm-check", command, kind="execute")
|
|
52
|
+
|
|
53
|
+
coro = request_permission_fn(
|
|
54
|
+
session_id=session_id,
|
|
55
|
+
tool_call=tool_call,
|
|
56
|
+
options=options,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
future = asyncio.run_coroutine_threadsafe(coro, loop)
|
|
61
|
+
response = future.result(timeout=timeout)
|
|
62
|
+
except (FutureTimeout, Exception) as exc:
|
|
63
|
+
logger.warning("Permission request timed out or failed: %s", exc)
|
|
64
|
+
return "deny"
|
|
65
|
+
|
|
66
|
+
outcome = response.outcome
|
|
67
|
+
if isinstance(outcome, AllowedOutcome):
|
|
68
|
+
option_id = outcome.option_id
|
|
69
|
+
# Look up the kind from our options list
|
|
70
|
+
for opt in options:
|
|
71
|
+
if opt.option_id == option_id:
|
|
72
|
+
return _KIND_TO_HERMES.get(opt.kind, "deny")
|
|
73
|
+
return "once" # fallback for unknown option_id
|
|
74
|
+
else:
|
|
75
|
+
return "deny"
|
|
76
|
+
|
|
77
|
+
return _callback
|