OUTPUT #691
PARÇA 3 / 5
TOPLAM: 167636 karakter | 2901 satır
BU PARÇA: 40000 karakter
use (OpenRouter format: provider/model). Defaults to anthropic/claude-
1499- sonnet-4.6.
1500- api_key (str): API key for authentication. Uses OPENROUTER_API_KEY env var if not provided.
1501: base_url (str): Base URL for the model API. Defaults to https://openrouter.ai/api/v1
1502- max_turns (int): Maximum number of API call iterations. Defaults to 10.
1503: enabled_toolsets (str): Comma-separated list of toolsets to enable. Supports predefined
1504: toolsets (e.g., "research", "development", "safe").
1505: Multiple toolsets can be combined: "web,vision"
1506: disabled_toolsets (str): Comma-separated list of toolsets to disable (e.g., "terminal")
1507- list_tools (bool): Just list available tools and exit
1508- save_trajectories (bool): Save conversation trajectories to JSONL files (appends to
1509- trajectory_samples.jsonl). Defaults to False.
1510- save_sample (bool): Save a single trajectory sample to a UUID-named JSONL file for inspection.
1511- Defaults to False.
1512- verbose (bool): Enable verbose logging for debugging. Defaults to False.
1513- log_prefix_chars (int): Number of characters to show in log previews for tool calls/responses.
1514- Defaults to 20.
1515-
1516: Toolset Examples:
1517: - "research": Web search, extract, crawl + vision tools
1518- """
1519- print("🤖 AI Agent with Tool Calling")
1520- print("=" * 50)
1521- if list_tools:
1522- return _print_tool_listing()
1523-
1524: enabled_toolsets_list = _parse_toolset_arg(enabled_toolsets, "🎯 Enabled toolsets")
1525: disabled_toolsets_list = _parse_toolset_arg(disabled_toolsets, "🚫 Disabled toolsets")
1526- if save_trajectories:
1527- print("💾 Trajectory saving: ENABLED")
1528- print(" - Successful conversations → trajectory_samples.jsonl")
1529- print(" - Failed conversations → failed_trajectories.jsonl")
1530-
1531- try:
1532- agent = AIAgent(
1533: base_url=base_url, model=model, api_key=api_key, max_iterations=max_turns,
1534: enabled_toolsets=enabled_toolsets_list, disabled_toolsets=disabled_toolsets_list,
1535- save_trajectories=save_trajectories, verbose_logging=verbose, log_prefix_chars=log_prefix_chars,
1536- )
1537- except RuntimeError as e:
1538- print(f"❌ Failed to initialize agent: {e}")
1539- return
1540-
--
1543- print(f"\n📝 User Query: {user_query}")
1544- print("\n" + "=" * 50)
1545-
1546- result = agent.run_conversation(user_query)
1547-
1548- print("\n" + "=" * 50 + "\n📋 CONVERSATION SUMMARY\n" + "=" * 50)
1549: print(f"✅ Completed: {result['completed']}\n📞 API Calls: {result['api_calls']}\n💬 Messages: {len(result['messages'])}")
1550- if result['final_response']:
1551- print("\n🎯 FINAL RESPONSE:\n" + "-" * 30 + "\n" + result['final_response'])
1552- if save_sample:
1553: _save_sample_trajectory(agent, result, user_query, model)
1554: print("\n👋 Agent execution completed!")
1555-
1556-
1557-if __name__ == "__main__":
1558- import fire
1559- fire.Fire(main)
1560-
--
1569-import copy # noqa: F401,E402
1570-import hashlib # noqa: F401,E402
1571-import tempfile # noqa: F401,E402
1572-
1573-
1574-_PLUGIN_COMPAT_LAZY = {
1575: 'COMPRESSED_SUMMARY_METADATA_KEY': ('agent.context_compressor', 'COMPRESSED_SUMMARY_METADATA_KEY'),
1576: 'ContextCompressor': ('agent.context_compressor', 'ContextCompressor'),
1577- 'DEFAULT_AGENT_IDENTITY': ('agent.prompt_builder', 'DEFAULT_AGENT_IDENTITY'),
1578- 'FailoverReason': ('agent.error_classifier', 'FailoverReason'),
1579- 'OpenAI': ('agent.process_bootstrap', 'OpenAI'),
1580- 'atomic_json_write': ('utils', 'atomic_json_write'),
1581: 'build_context_files_prompt': ('agent.prompt_builder', 'build_context_files_prompt'),
1582- 'build_environment_hints': ('agent.prompt_builder', 'build_environment_hints'),
1583- 'build_skills_system_prompt': ('agent.prompt_builder', 'build_skills_system_prompt'),
1584: 'check_toolset_requirements': ('model_tools', 'check_toolset_requirements'),
1585- 'convert_scratchpad_to_think': ('agent.trajectory', 'convert_scratchpad_to_think'),
1586: 'estimate_request_tokens_rough': ('agent.model_metadata', 'estimate_request_tokens_rough'),
1587- 'file_mutation_result_landed': ('agent.tool_result_classification', 'file_mutation_result_landed'),
1588- 'flatten_message_text': ('agent.message_content', 'flatten_message_text'),
1589: 'get_tool_definitions': ('model_tools', 'get_tool_definitions'),
1590: 'handle_function_call': ('model_tools', 'handle_function_call'),
1591- 'is_truthy_value': ('utils', 'is_truthy_value'),
1592: 'jittered_backoff': ('agent.retry_utils', 'jittered_backoff'),
1593- 'load_soul_md': ('agent.prompt_builder', 'load_soul_md'),
1594- 'normalize_usage': ('agent.usage_pricing', 'normalize_usage'),
1595- 'redact_sensitive_text': ('agent.redact', 'redact_sensitive_text'),
1596- 'request_hard_interrupt': ('agent.interrupt_compat', 'request_hard_interrupt'),
1597: 'sanitize_context': ('agent.memory_manager', 'sanitize_context'),
1598: 'user_originated_turn_view': ('agent.context_compressor', 'user_originated_turn_view'),
1599-}
1600-
1601-
1602-def __getattr__(name): # PEP 562 — lazy so no import cycles
1603- target = _PLUGIN_COMPAT_LAZY.get(name)
1604- if target is None:
===== /home/hermes/.hermes/hermes-agent/setup.py =====
70- return super().run(*args, **kwargs)
71-
72- cmdclass["bdist_wheel"] = _GuardedBdistWheel
73-except ImportError:
74- pass
75-
76:# Root single-file modules (``run_agent``, ``hermes_state``, ``toolsets``...)
77-# are invisible to ``packages.find``: that finder sees only directories with an
78-# ``__init__.py``. The wheel build needs them on ``py_modules``, so derive the
79-# list from the source tree at build time. A static list in ``pyproject.toml``
80-# drifted each time the tree layout changed (missing modules broke installed
81-# wheels with ``ModuleNotFoundError``), so there is no list to maintain here.
82-# ``setup()`` kwargs merge with ``pyproject.toml``, and this file is the only
===== /home/hermes/.hermes/hermes-agent/toolset_distributions.py =====
1-#!/usr/bin/env python3
2:"""Toolset distributions for batch data-generation runs.
3-
4:A distribution maps toolset names to the % chance each is enabled for a prompt
5:(sampled independently, so several toolsets can be active at once). A key may
6-be a "+"-grouped compound ("browser+search") that rolls once for all members.
7-"""
8-
9-from typing import Dict, List, Optional
10-import random
11:from toolsets import validate_toolset
12-
13-
14:def _dist(description: str, **toolsets: int) -> Dict[str, object]:
15: return {"description": description, "toolsets": toolsets}
16-
17-
18-DISTRIBUTIONS = {
19: "default": _dist("All available tools, all the time", web=100, vision=100, image_gen=100, terminal=100, file=100, browser=100),
20: "image_gen": _dist("Heavy focus on image generation with vision and web support", image_gen=90, vision=90, web=55, terminal=45),
21: "research": _dist("Web research with vision analysis and reasoning", web=90, browser=70, vision=50, terminal=10),
22- "science": _dist("Scientific research with web, terminal, file, and browser capabilities",
23: web=94, terminal=94, file=94, vision=65, browser=50, image_gen=15),
24: "development": _dist("Terminal, file tools, and reasoning with occasional web lookup", terminal=80, file=80, web=30, vision=10),
25: "safe": _dist("All tools except terminal for safety", web=80, browser=70, vision=60, image_gen=60),
26: "balanced": _dist("Equal probability of all toolsets", web=50, vision=50, image_gen=50, terminal=50, file=50, browser=50),
27- "minimal": _dist("Only web tools for basic research", web=100),
28- "terminal_only": _dist("Terminal and file tools for code execution tasks", terminal=100, file=100),
29- "terminal_web": _dist("Terminal and file tools with web search for documentation lookup", terminal=100, file=100, web=100),
30: "creative": _dist("Image generation and vision analysis focus", image_gen=90, vision=90, web=30),
31: "reasoning": _dist("Heavy research/reasoning distribution with minimal other tools", web=90, file=60, terminal=20),
32: "browser_use": _dist("Full browser-based web interaction with search, vision, and page control", browser=100, web=80, vision=70),
33- "browser_only": _dist("Only browser automation tools for pure web interaction tasks", browser=100),
34- # browser-use-tasks.jsonl: one grouped roll keeps web_search (for finding URLs) coupled to browser
35- # at the original 97% now that `browser` no longer bundles it (#64503).
36- "browser_tasks": _dist(
37- "Browser-focused distribution with web_search for finding URLs (Google blocks direct browser searches)",
38: **{"browser+search": 97}, vision=12, terminal=15,
39- ),
40- # nous-terminal-tasks.jsonl
41- "terminal_tasks": _dist("Terminal-focused distribution with high terminal/file availability, occasional other tools",
42: terminal=97, file=97, web=97, browser=75, vision=50, image_gen=10),
43- # mixed-browser-terminal-tasks.jsonl
44- "mixed_tasks": _dist("Mixed distribution with high browser, terminal, and file availability for complex tasks",
45: browser=92, terminal=92, file=92, web=35, vision=15, image_gen=15),
46-}
47-
48-
49-def get_distribution(name: str) -> Optional[Dict[str, any]]:
50: """Distribution definition (description + toolsets), or None if unknown."""
51- return DISTRIBUTIONS.get(name)
52-
53-
54-def list_distributions() -> Dict[str, Dict]:
55- return DISTRIBUTIONS.copy()
56-
57-
58-def validate_distribution(distribution_name: str) -> bool:
59- return distribution_name in DISTRIBUTIONS
60-
61-
62-def _entry_members(entry: str) -> List[str]:
63: """Toolsets named by a distribution entry: a bare name or a "+"-grouped compound."""
64- return [name.strip() for name in entry.split("+")]
65-
66-
67:def sample_toolsets_from_distribution(distribution_name: str) -> List[str]:
68: """Sample toolset names, each entry included independently with its % probability.
69-
70: An entry may be a single toolset or a "+"-grouped compound like
71- "browser+search": one roll selects (or skips) every member together, so
72- co-occurrence guarantees survive that independent rolls would break
73- (two independent 97% rolls co-occur only ~94% of the time).
74- Falls back to the highest-probability entry when nothing was rolled.
75- Raises ValueError for an unknown distribution.
76- """
77- dist = get_distribution(distribution_name)
78- if not dist:
79- raise ValueError(f"Unknown distribution: {distribution_name}")
80: selected_toolsets = []
81: for entry, probability in dist["toolsets"].items():
82- members = _entry_members(entry)
83: invalid = [name for name in members if not validate_toolset(name)]
84- if invalid:
85: print(f"⚠️ Warning: Toolset '{'+'.join(invalid)}' in distribution '{distribution_name}' is not valid")
86- elif random.random() * 100 < probability:
87: selected_toolsets.extend(members)
88: if not selected_toolsets and dist["toolsets"]:
89: highest_prob_entry = max(dist["toolsets"].items(), key=lambda x: x[1])[0]
90- members = _entry_members(highest_prob_entry)
91: if all(validate_toolset(name) for name in members):
92: selected_toolsets.extend(members)
93: return selected_toolsets
94-
95-
96-def print_distribution_info(distribution_name: str) -> None:
97: """Print a distribution's description and toolset probabilities (highest first)."""
98- dist = get_distribution(distribution_name)
99- if not dist:
100- print(f"❌ Unknown distribution: {distribution_name}")
101- return
102- print(f"\n📊 Distribution: {distribution_name}")
103- print(f" Description: {dist['description']}")
104: print(" Toolsets:")
105: for toolset, prob in sorted(dist["toolsets"].items(), key=lambda x: x[1], reverse=True):
106: print(f" • {toolset:15} : {prob:3}% chance")
===== /home/hermes/.hermes/hermes-agent/toolsets.py =====
1:"""Toolset helpers: get/resolve/validate named tool groups (static TOOLSETS + registry-registered)."""
2-
3-from typing import Dict, List, Any, Set, Optional, Tuple
4-
5-
6:# Shared tool list for CLI and all messaging platform toolsets (edit once, all
7-# platforms follow). Desktop GUI affordances are deliberately NOT here: they live
8-# in `desktop_ui`/`project`, enabled per desktop-sourced session by the GUI gateway
9:# (tui_gateway/server.py::_load_enabled_toolsets). HA, kanban and computer_use
10-# entries are further gated by their tools' check_fns.
11-_HERMES_CORE_TOOLS = [
12- "web_search", "web_extract",
13- "terminal", "process_manage",
14- "read_file", "write_file", "patch", "search_files",
15: "vision_analyze", "image_generate",
16- "skills_list", "skill_view", "skill_manage",
17- "browser_navigate", "browser_snapshot", "browser_click",
18- "browser_type", "browser_scroll", "browser_back",
19- "browser_press", "browser_get_images",
20: "browser_vision", "browser_console", "browser_cdp", "browser_dialog",
21- "browser_exec", # replaces the other browser tools when browser.backend is "browser-use"
22- "text_to_speech",
23- "todo_list", "memory",
24- "session_search",
25- "clarify",
26: "execute_code", "delegate_task",
27- "cronjob_manage",
28- "ha_list_entities", "ha_get_state", "ha_list_services", "ha_call_service",
29- "kanban_show", "kanban_list",
30: "kanban_complete", "kanban_block", "kanban_request_review",
31- "kanban_request_changes",
32- "kanban_heartbeat",
33- "kanban_comment", "kanban_create", "kanban_link",
34- "kanban_unblock",
35- "kanban_attach", "kanban_attach_url", "kanban_attachments",
36- "computer_use",
37-]
38-
39-# Webhook payloads are untrusted third-party content: no file/system execution.
40:_HERMES_WEBHOOK_SAFE_TOOLS = ["web_search", "web_extract", "vision_analyze", "clarify"]
41-_HA_TOOLS = ["ha_list_entities", "ha_get_state", "ha_list_services", "ha_call_service"]
42-_FEISHU_TOOLS = [
43- "feishu_doc_read", "feishu_drive_list_comments", "feishu_drive_list_comment_replies",
44- "feishu_drive_reply_comment", "feishu_drive_add_comment",
45-]
46-_YUANBAO_TOOLS = ["yb_query_group_info", "yb_query_group_members", "yb_send_dm", "yb_search_sticker", "yb_send_sticker"]
47-
48-
49-def _ts(description, tools=(), includes=(), **extra):
50: """One TOOLSETS entry (fresh lists per entry; extra keys such as posture pass through)."""
51- return {"description": description, "tools": list(tools), "includes": list(includes), **extra}
52-
53-
54-def _bundle(description, extras=()):
55- """A `hermes-*` platform bundle: the shared core tools plus optional platform extras."""
56- return _ts(description, _HERMES_CORE_TOOLS + list(extras))
--
62-
63-
64-# Coding posture: everything you reach for while pairing on code; drops messaging,
65-# tts, image_gen, home-assistant, cron, kanban and computer-use.
66-_CODING_TOOLS = _core_without("image_generate", "text_to_speech", "cronjob_manage", "computer_use", *_HA_TOOLS, kanban=False)
67-
68:# Core toolset definitions: individual tools or references to other toolsets.
69:TOOLSETS = {
70: # Basic toolsets - individual tool categories
71- "web": _ts("Web research and content extraction tools", ["web_search", "web_extract"]),
72- "search": _ts("Web search only (no content extraction/scraping)", ["web_search"]),
73- "x_search": _ts(
74- "Search X (Twitter) posts and threads via xAI's built-in x_search Responses "
75- "tool. Read-only public X discovery; use the xurl skill for authenticated X "
76- "API reads and account actions. Available when xAI credentials are configured "
77- "(SuperGrok OAuth or XAI_API_KEY). Off by default; enable in `hermes tools` → "
78- "X (Twitter) Search.",
79- ["x_search"],
80- ),
81: "vision": _ts("Image analysis and vision tools", ["vision_analyze"]),
82: "video": _ts("Video analysis and understanding tools (opt-in, not in default toolset)", ["video_analyze"]),
83- "image_gen": _ts("Creative generation tools (images)", ["image_generate"]),
84- "video_gen": _ts(
85- "Video generation tools. Single ``video_generate`` tool covers text-to-video "
86- "(prompt only) and image-to-video (prompt + image_url), plus "
87- "reference-to-video. Provider-specific edit/extend workflows may appear as "
88- "separate tools. Configure via ``hermes tools`` → Video Generation.",
89- ["video_generate", "xai_video_edit", "xai_video_extend"],
90- ),
91- "computer_use": _ts(
92: "Background desktop control via cua-driver (macOS/Windows/Linux) — "
93- "screenshots, mouse, keyboard, scroll, drag. Does NOT steal the user's cursor "
94: "or keyboard focus. Works with any tool-capable model.",
95- ["computer_use"],
96- ),
97- "terminal": _ts("Terminal/command execution and process management tools", ["terminal", "process_manage"]),
98- "skills": _ts(
99- "Access, create, edit, and manage skill documents with specialized "
100- "instructions and knowledge",
101- ["skills_list", "skill_view", "skill_manage"],
102- ),
103- # web_search belongs to `web`/`search` only. Listing it here too let
104: # `disabled_toolsets: [browser]` (headless/Docker deployments) strip
105: # web_search from every session, because disabled toolsets are a strict
106- # end-of-pipeline subtraction (#17309, #64503).
107- "browser": _ts(
108- "Browser automation for web interaction (navigate, click, type, scroll, "
109- "iframes, hold-click)",
110- [t for t in _HERMES_CORE_TOOLS if t.startswith("browser_")],
111- ),
--
119- "search (content + files)",
120- ["read_file", "write_file", "patch", "search_files"],
121- ),
122- "tts": _ts("Text-to-speech: convert text to audio with Edge TTS (free), ElevenLabs, OpenAI, or xAI", ["text_to_speech"]),
123- "todo": _ts("Task planning and tracking for multi-step work", ["todo_list"]),
124- "memory": _ts("Persistent memory across sessions (personal notes + user profile)", ["memory"]),
125: "context_engine": _ts("Runtime tools exposed by the active context engine"),
126- "session_search": _ts("Search and recall past conversations with summarization", ["session_search"]),
127- "project": _ts("Desktop Projects — create/switch named workspaces (GUI sessions only)", ["desktop_project"]),
128- "bot_room": _ts("Verified text-only Group Chat turn capabilities"),
129-
130- # GUI-renderer affordances, enabled per desktop-sourced SESSION by the GUI
131: # gateway (tui_gateway/server.py::_load_enabled_toolsets) — never by a
132- # process env var, which is blind to a desktop client on a remote backend.
133- "desktop_ui": _ts(
134- "Desktop GUI affordances — in-app terminal/browser panes, pane focus, "
135- "reactions (GUI sessions only)",
136- ["read_terminal", "close_terminal", "desktop_preview", "drive_preview",
137- "annotate_preview", "read_window_below", "focus_pane", "react_to_message",
138- "setup_mcp", "gui_tour", "show_tip"],
139- ),
140- "clarify": _ts("Ask the user clarifying questions (multiple-choice or open-ended)", ["clarify"]),
141- "code_execution": _ts("Run Python scripts that call tools programmatically (reduces LLM round trips)", ["execute_code"]),
142: "delegation": _ts("Spawn subagents with isolated context for complex subtasks", ["delegate_task"]),
143- "homeassistant": _ts("Home Assistant smart home control and monitoring", _HA_TOOLS),
144- "kanban": _ts(
145- "Kanban multi-agent coordination — only active when the agent is spawned by "
146- "the kanban dispatcher (HERMES_KANBAN_TASK env set). The dispatcher runs "
147- "inside the gateway by default; see `kanban.dispatch_in_gateway` in "
148- "config.yaml. Lets workers mark tasks done with structured handoffs, enter "
--
159- "spotify": _ts(
160- "Native Spotify playback, search, playlist, album, and library tools",
161- ["spotify_playback", "spotify_devices", "spotify_queue", "spotify_search",
162- "spotify_playlists", "spotify_albums", "spotify_library"],
163- ),
164-
165: # Scenario-specific toolsets
166- "debugging": _ts("Debugging and troubleshooting toolkit", ["terminal", "process_manage"], includes=["web", "file"]),
167: "safe": _ts("Safe toolkit without terminal access", [], includes=["web", "vision", "image_gen"]),
168-
169: # Coding posture, auto-selected in a code workspace (agent/coding_context.py).
170- # `desktop_ui` is folded in separately by the GUI gateway for desktop sessions.
171- # posture=True: per-session posture, never auto-recovered into platform tool
172: # config (see the non-configurable-toolset recovery loop in hermes_cli/tools_config.py).
173- "coding": _ts(
174: "Coding-focused toolset: files, terminal, search, web docs, skills, todo, "
175: "delegate, vision, browser",
176- _CODING_TOOLS,
177- posture=True,
178- ),
179-
180: # Full Hermes toolsets (CLI + messaging platforms). All share the core tools;
181- # there is deliberately no agent-callable send_message tool. hermes-acp is the
182- # coding posture minus the interactive clarify UI.
183- "hermes-acp": _ts(
184- "Editor integration (VS Code, Zed, JetBrains) — coding-focused tools without "
185- "messaging, audio, or clarify UI",
186- [t for t in _CODING_TOOLS if t != "clarify"],
187- ),
188- "hermes-api-server": _ts(
189- "OpenAI-compatible API server — full agent tools accessible via HTTP (no "
190- "interactive UI tools like clarify or send_message)",
191- _core_without("text_to_speech", "clarify", "computer_use", kanban=False),
192- ),
193: "hermes-cli": _bundle("Full interactive CLI toolset - all default tools plus cronjob management"),
194-
195- # Mirrors hermes-cli; `hermes tools` platform config filters it down and
196: # _get_platform_tools() drops _DEFAULT_OFF_TOOLSETS unless user-enabled.
197: "hermes-cron": _bundle("Default cron toolset - same core tools as hermes-cli; gated by `hermes tools`"),
198: "hermes-telegram": _bundle("Telegram bot toolset - full access for personal use (terminal has safety checks)"),
199- "hermes-discord": _bundle(
200: "Discord bot toolset - full access (terminal has safety checks via dangerous "
201: "command approval)",
202- ["discord", "discord_admin"],
203- ),
204: "hermes-whatsapp": _bundle("WhatsApp bot toolset - similar to Telegram (personal messaging, more trusted)"),
205: "hermes-slack": _bundle("Slack bot toolset - full access for workspace use (terminal has safety checks)"),
206: "hermes-signal": _bundle("Signal bot toolset - encrypted messaging platform (full access)"),
207: "hermes-bluebubbles": _bundle("BlueBubbles iMessage bot toolset - Apple iMessage via local BlueBubbles server"),
208: "hermes-homeassistant": _bundle("Home Assistant bot toolset - smart home event monitoring and control"),
209: "hermes-email": _bundle("Email bot toolset - interact with Hermes via email (IMAP/SMTP)"),
210: "hermes-mattermost": _bundle("Mattermost bot toolset - self-hosted team messaging (full access)"),
211: "hermes-matrix": _bundle("Matrix bot toolset - decentralized encrypted messaging (full access)"),
212: "hermes-dingtalk": _bundle("DingTalk bot toolset - enterprise messaging platform (full access)"),
213: "hermes-feishu": _bundle("Feishu/Lark bot toolset - enterprise messaging via Feishu/Lark (full access)", _FEISHU_TOOLS),
214: "hermes-weixin": _bundle("Weixin bot toolset - personal WeChat messaging via iLink (full access)"),
215: "hermes-qqbot": _bundle("QQBot toolset - QQ messaging via Official Bot API v2 (full access)"),
216: "hermes-wecom": _bundle("WeCom bot toolset - enterprise WeChat messaging (full access)"),
217: "hermes-wecom-callback": _bundle("WeCom callback toolset - enterprise self-built app messaging (full access)"),
218- "hermes-yuanbao": {
219- "description": "Yuanbao Bot 元宝消息平台工具集 - 群信息、成员查询、私聊、贴纸表情",
220- "tools": _HERMES_CORE_TOOLS + _YUANBAO_TOOLS,
221- "module": "tools.yuanbao_tools",
222- "includes": [],
223- },
224: "hermes-sms": _bundle("SMS bot toolset - interact with Hermes via SMS (Twilio)"),
225: "hermes-webhook": _ts("Webhook toolset - receive and process external webhook events", _HERMES_WEBHOOK_SAFE_TOOLS),
226- "hermes-gateway": _ts(
227: "Gateway toolset - union of all messaging platform tools",
228- [],
229- includes=[
230- "hermes-telegram", "hermes-discord", "hermes-whatsapp", "hermes-slack",
231- "hermes-signal", "hermes-bluebubbles", "hermes-homeassistant", "hermes-email",
232- "hermes-sms", "hermes-mattermost", "hermes-matrix", "hermes-dingtalk",
233- "hermes-feishu", "hermes-wecom", "hermes-wecom-callback", "hermes-weixin",
--
256-
257-def _registry_generation() -> Tuple[int, int]:
258- reg = _registry()
259- return (id(reg), getattr(reg, "_generation", 0)) if reg is not None else (0, 0)
260-
261-
262:def get_toolset(name: str, *, include_registry: bool = True) -> Optional[Dict[str, Any]]:
263: """Toolset definition, or None if unknown.
264-
265: include_registry=True merges plugin/overlay tools registered into this toolset
266: and resolves registry-only (plugin/MCP) toolsets and aliases; False returns a
267: copy of the static TOOLSETS entry only, so platform reverse-mapping is
268- unaffected by registry additions.
269-
270: Args: name (str): Name of the toolset include_registry (bool): When True (default), merge in tools that
271: plugins/overlays registered into this toolset via the registry. Platform reverse-mapping in
272: ``_get_platform_tools`` uses False so that a tool registered into a toolset but absent from a platform's
273: static composite does not drop the whole toolset from inference. See issue #49622.
274- """
275: toolset = TOOLSETS.get(name)
276- if not include_registry:
277: return {**toolset, "tools": list(toolset.get("tools", [])), "includes": list(toolset.get("includes", []))} if toolset else None
278-
279- registry = _registry()
280- if registry is None:
281: return toolset if toolset else None
282-
283: if toolset:
284: merged_tools = set(toolset.get("tools", [])) | set(registry.get_tool_names_for_toolset(name))
285: # An MCP server named like a built-in toolset ("homeassistant", "browser") registers a bare
286: # alias to its `mcp-<name>` toolset; without this union the static entry shadows it and the
287: # server's tools never reach the model even though discovery registered them.
288: alias_target = registry.get_toolset_alias_target(name)
289- if alias_target and alias_target != name:
290: merged_tools |= set(registry.get_tool_names_for_toolset(alias_target))
291: return {**toolset, "tools": sorted(merged_tools)}
292-
293: if name in _get_plugin_toolset_names():
294: # Plugin toolset; shown as its MCP server alias when one exists.
295: registry_toolset = name
296: alias = _display_alias(name, _get_registry_toolset_aliases())
297: description = f"MCP server '{alias}' tools" if alias else f"Plugin toolset: {name}"
298- else:
299: registry_toolset = registry.get_toolset_alias_target(name)
300: if not registry_toolset:
301- return None
302- description = f"MCP server '{name}' tools"
303: return {"description": description, "tools": registry.get_tool_names_for_toolset(registry_toolset), "includes": []}
304-
305-
306:def bundle_non_core_tools(toolset_name: str) -> Set[str]:
307- """A bundle's tools minus _HERMES_CORE_TOOLS (one level of includes).
308-
309- Disabling a `core + extras` bundle must not strip the core tools every other
310: toolset shares. One `includes` pass suffices (only hermes-gateway nests
311- bundles). Unknown names: full resolution minus core.
312- """
313- core = set(_HERMES_CORE_TOOLS)
314: ts_def = get_toolset(toolset_name)
315- if not (ts_def and "tools" in ts_def):
316: return set(resolve_toolset(toolset_name)) - core
317- to_remove = set(ts_def["tools"])
318: for inc_def in map(get_toolset, ts_def.get("includes", [])):
319- if inc_def and "tools" in inc_def:
320- to_remove.update(inc_def["tools"])
321- return to_remove - core
322-
323-
324-# Memo keyed on (name, include_registry, id(registry), registry generation);
325-# engages only at the public entry (visited is None).
326:_resolve_toolset_memo: Dict[Tuple[str, bool, int, int], List[str]] = {}
327-
328-
329-def _plugin_platform_bundle(name: str) -> List[str]:
330- """Implicit `hermes-<platform>` bundle for a registered plugin platform: core
331- tools plus whatever the plugin registered under the platform name. [] otherwise."""
332- if not name.startswith("hermes-"):
--
337- if not platform_registry.is_registered(platform_name):
338- return []
339- except Exception:
340- return []
341- tools = set(_HERMES_CORE_TOOLS)
342- try:
343: tools.update(e.name for e in _registry_call("get_all_entries", ()) if e.toolset == platform_name)
344- except Exception:
345- pass
346- return list(tools)
347-
348-
349:def resolve_toolset(name: str, visited: Set[str] = None, *, include_registry: bool = True) -> List[str]:
350: """Recursively resolve a toolset (and its includes) to a sorted tool-name list.
351: include_registry=False resolves the static TOOLSETS view only.
352-
353: Args: name (str): Name of the toolset to resolve visited (Set[str]): Set of already visited toolsets
354- (for cycle detection) include_registry (bool): When True (default), include tools that plugins/overlays
355: registered into a toolset. Platform reverse-mapping uses False so a registry-added tool cannot drop the
356: whole toolset from inference (see #49622 and ``_get_platform_tools``).
357- """
358- external_call = visited is None
359- if external_call:
360- memo_key = (name, include_registry, *_registry_generation())
361: cached = _resolve_toolset_memo.get(memo_key)
362- if cached is not None:
363- return list(cached)
364- visited = set()
365-
366: # "all"/"*" span every toolset so new toolsets are included automatically.
367- if name in {"all", "*"}:
368- all_tools: Set[str] = set()
369: for toolset_name in get_toolset_names():
370: all_tools.update(resolve_toolset(toolset_name, visited.copy(), include_registry=include_registry))
371- return sorted(all_tools)
372-
373- # Diamond include or cycle: [] silently — the tools are collected via another path.
374- if name in visited:
375- return []
376- visited.add(name)
377-
378: toolset = get_toolset(name, include_registry=include_registry)
379: if not toolset:
380- return _plugin_platform_bundle(name) if include_registry else []
381-
382: tools = set(toolset.get("tools", []))
383: for included_name in toolset.get("includes", []):
384: tools.update(resolve_toolset(included_name, visited, include_registry=include_registry))
385-
386- result = sorted(tools)
387- if external_call:
388: if len(_resolve_toolset_memo) >= 256: # stale-generation entries are never hit again
389: _resolve_toolset_memo.clear()
390: _resolve_toolset_memo[memo_key] = list(result)
391- return result
392-
393-
394:def _get_plugin_toolset_names() -> Set[str]:
395: """Registry toolset names absent from the static TOOLSETS dict."""
396: return {n for n in _registry_call("get_registered_toolset_names", ()) if n not in TOOLSETS}
397-
398-
399:def _get_registry_toolset_aliases() -> Dict[str, str]:
400: return _registry_call("get_registered_toolset_aliases", {})
401-
402-
403-def _display_alias(ts_name: str, aliases: Dict[str, str]) -> Optional[str]:
404- """First non-static alias pointing at *ts_name*, or None."""
405: return next((a for a, canonical in aliases.items() if canonical == ts_name and a not in TOOLSETS), None)
406-
407-
408-def _plugin_display_names() -> List[str]:
409: """Plugin toolset names, shown under their first non-static alias when one exists."""
410: aliases = _get_registry_toolset_aliases()
411: return [_display_alias(n, aliases) or n for n in _get_plugin_toolset_names()]
412-
413-
414:def get_all_toolsets() -> Dict[str, Dict[str, Any]]:
415: """All toolset definitions: static plus plugin-registered."""
416: result = dict(TOOLSETS)
417: aliases = _get_registry_toolset_aliases()
418- for display_name in _plugin_display_names():
419: toolset = None if display_name in result else get_toolset(display_name)
420: if toolset:
421: result[display_name] = toolset
422: # Static names an MCP server also aliases show the merged view get_toolset() resolves.
423: for name in TOOLSETS.keys() & aliases.keys():
424: result[name] = get_toolset(name) or result[name]
425- return result
426-
427-
428:def get_toolset_names() -> List[str]:
429: """Sorted names of all toolsets (static + plugin), excluding aliases."""
430: return sorted(set(TOOLSETS.keys()) | set(_plugin_display_names()))
431-
432-
433:def validate_toolset(name: str) -> bool:
434: return (name in {"all", "*"} or name in TOOLSETS
435: or name in _get_plugin_toolset_names() or name in _get_registry_toolset_aliases())
436-
437-
438:def create_custom_toolset(name: str, description: str, tools: List[str] = None, includes: List[str] = None) -> None:
439: """Register a runtime toolset in TOOLSETS."""
440: TOOLSETS[name] = _ts(description, tools or [], includes or [])
441-
442-
443:def get_toolset_info(name: str) -> Dict[str, Any]:
444: """Toolset definition plus its resolved tools, or None if unknown."""
445: toolset = get_toolset(name)
446: if not toolset:
447- return None
448: resolved_tools = resolve_toolset(name)
449- return {
450: "name": name, "description": toolset["description"],
451: "direct_tools": toolset["tools"], "includes": toolset["includes"],
452- "resolved_tools": resolved_tools, "tool_count": len(resolved_tools),
453: "is_composite": bool(toolset["includes"]),
454- }
455-
456-
457-# ---- BEGIN PLUGIN-COMPAT (revert-scheduled; see COMPAT_MANIFEST.md) ----
458-# Names external plugins imported from this module before the Sep 2026 decomposition.
459-# Internal code MUST NOT use these (scripts/check_compat_pointers.py fails CI if it does).
460-# The whole block is removed by reverting the commit that added it.
461-
462:def resolve_multiple_toolsets(toolset_names: List[str]) -> List[str]:
463- """
464: Resolve multiple toolsets and combine their tools.
465-
466- Args:
467: toolset_names (List[str]): List of toolset names to resolve
468-
469- Returns:
470- List[str]: Combined list of all tool names (deduplicated)
471- """
472- all_tools = set()
473-
474: for name in toolset_names:
475: tools = resolve_toolset(name)
476- all_tools.update(tools)
477-
478- return sorted(all_tools)
479-# ---- END PLUGIN-COMPAT ----
===== /home/hermes/.hermes/hermes-agent/trajectory_compressor.py =====
1-#!/usr/bin/env python3
2:"""Trajectory Compressor — post-process agent trajectories into a token budget.
3-
4-Strategy: protect the head (system, human, first gpt, first tool) and the last N
5-turns; from the middle, summarize only as many turns as needed (never splitting a
6-<tool_call>/<tool_response> pair) and replace them with one human summary turn.
7-
8-Usage:
9: python trajectory_compressor.py --input=data/my_run # directory
10: python trajectory_compressor.py --input=data/trajectories.jsonl --sample_percent=15
11: python trajectory_compressor.py --input=data/trajectories.jsonl --output=out.jsonl --target_max_tokens=16000
12-"""
13-
14-import json
15-import os
16-import random
17-import shutil
--
27-
28-from utils import base_url_host_matches, base_url_hostname
29-import fire
30-from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn, TimeElapsedColumn, TimeRemainingColumn
31-from rich.console import Console
32-from hermes_constants import OPENROUTER_BASE_URL, get_hermes_home
33:from agent.retry_utils import jittered_backoff
34-from hermes_cli.env_loader import load_hermes_dotenv
35-
36:# Load .env from HERMES_HOME first, then project root as a dev fallback.
37-load_hermes_dotenv(hermes_home=get_hermes_home(), project_env=Path(__file__).parent / ".env")
38-
39-
40:def _response_finish_reason(response: Any) -> str:
41: """Lowercased ``choices[0].finish_reason`` of a dict/object response, ``""`` if absent.
42-
43: Local copy of ``agent.context_compressor._response_finish_reason``: this
44: standalone CLI deliberately avoids importing the heavy context compressor.
45- """
46- try:
47- choices = (response.get("choices") if isinstance(response, dict) else getattr(response, "choices", None)) or []
48- first = choices[0] if choices else None
49: reason = first.get("finish_reason") if isinstance(first, dict) else getattr(first, "finish_reason", None)
50- return str(reason).strip().lower() if reason else ""
51- except Exception:
52- return ""
53-
54-
55:def _effective_temperature_for_model(model: str, requested_temperature: Optional[float], base_url: Optional[str] = None) -> Optional[float]:
56: """Apply fixed model temperature contracts to direct client calls.
57-
58: Returns ``None`` when the model manages temperature server-side (Kimi);
59- callers must omit the ``temperature`` kwarg entirely in that case.
60- Shared with ``mini_swe_runner`` (which passes ``requested_temperature=None``).
61- """
62- try:
63: from agent.auxiliary_client import _fixed_temperature_for_model, OMIT_TEMPERATURE
64- except Exception:
65- return requested_temperature
66: fixed_temperature = _fixed_temperature_for_model(model, base_url)
67- if fixed_temperature is OMIT_TEMPERATURE:
68- return None # caller must omit temperature
69- return requested_temperature if fixed_temperature is None else fixed_temperature
70-
71-
72-def _load_jsonl(path: Path, on_error: Optional[Callable[[int, json.JSONDecodeError], None]] = None, start: int = 0) -> List[Tuple[int, Any]]: