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]]: