|
3 | 3 | """ |
4 | 4 | NemoClaw plugin for Hermes Agent. |
5 | 5 |
|
6 | | -Provides sandbox status tools and skill hot-reload when Hermes runs inside an |
7 | | -OpenShell sandbox managed by NemoClaw. |
| 6 | +Provides sandbox status tools, skill hot-reload, managed-tool broker patches, |
| 7 | +and quiet runtime grounding when Hermes runs inside an OpenShell sandbox |
| 8 | +managed by NemoClaw. |
8 | 9 |
|
9 | 10 | Skill hot-reload: Hermes caches its skill slash-command registry in a |
10 | 11 | module-global dict on first scan. New skills dropped on disk are invisible |
11 | 12 | until the cache is cleared. This plugin provides a nemoclaw_reload_skills |
12 | 13 | tool that clears the cache and re-scans, letting the agent pick up new |
13 | 14 | skills without a gateway restart. The on_session_start hook also refreshes |
14 | 15 | skills automatically at session boundaries. |
| 16 | +
|
| 17 | +Runtime grounding: earlier versions injected a visible startup banner, but |
| 18 | +Hermes TUI renders plugin-injected messages through the interrupt queue. This |
| 19 | +plugin now uses Hermes' pre_llm_call context hook so the model sees the |
| 20 | +NemoClaw sandbox/tool-execution topology without leaking visual noise into the |
| 21 | +chat transcript. |
15 | 22 | """ |
16 | 23 |
|
17 | 24 | import atexit |
|
41 | 48 | "modal": "MODAL_GATEWAY_URL", |
42 | 49 | } |
43 | 50 |
|
| 51 | +_NEMOCLAW_CONTEXT_KEYWORDS = ( |
| 52 | + "browser", |
| 53 | + "config", |
| 54 | + "discord", |
| 55 | + "environment", |
| 56 | + "gateway", |
| 57 | + "hermes", |
| 58 | + "host", |
| 59 | + "logs", |
| 60 | + "modal", |
| 61 | + "nemoclaw", |
| 62 | + "openshell", |
| 63 | + "sandbox", |
| 64 | + "skill", |
| 65 | + "slack", |
| 66 | + "status", |
| 67 | + "telegram", |
| 68 | + "tool", |
| 69 | + "where am i", |
| 70 | + "whoami", |
| 71 | +) |
| 72 | + |
44 | 73 |
|
45 | 74 | def _get_env_value(key, default=None): |
46 | 75 | """Read env from os.environ, then Hermes' dotenv-aware config loader.""" |
@@ -858,6 +887,81 @@ def _get_sandbox_info(): |
858 | 887 | } |
859 | 888 |
|
860 | 889 |
|
| 890 | +def _active_managed_gateway_services(): |
| 891 | + """List managed Nous services that have broker URLs configured.""" |
| 892 | + services = [] |
| 893 | + for service, env_key in _TOOL_GATEWAY_URL_ENV.items(): |
| 894 | + if _get_env_value(env_key, ""): |
| 895 | + services.append(service) |
| 896 | + return services |
| 897 | + |
| 898 | + |
| 899 | +def _should_inject_nemoclaw_context(user_message=None, is_first_turn=False): |
| 900 | + """Return whether this turn needs NemoClaw runtime grounding.""" |
| 901 | + if is_first_turn: |
| 902 | + return True |
| 903 | + text = str(user_message or "").lower() |
| 904 | + return any(keyword in text for keyword in _NEMOCLAW_CONTEXT_KEYWORDS) |
| 905 | + |
| 906 | + |
| 907 | +def _build_nemoclaw_agent_context(platform=None): |
| 908 | + """Build quiet, ephemeral context for Hermes' pre_llm_call hook.""" |
| 909 | + info = _get_sandbox_info() |
| 910 | + hermes_home = ( |
| 911 | + os.getenv("HERMES_HOME") |
| 912 | + or _get_env_value("HERMES_HOME", "") |
| 913 | + or "/sandbox/.hermes-data" |
| 914 | + ) |
| 915 | + services = _active_managed_gateway_services() |
| 916 | + service_text = ", ".join(services) if services else "none detected" |
| 917 | + broker_state = "enabled" if _broker_mode_enabled() else "not enabled" |
| 918 | + platform_text = str(platform or "").strip() |
| 919 | + platform_line = ( |
| 920 | + f"- Current Hermes messaging platform: {platform_text}. Messaging adapters " |
| 921 | + "run in the parent Hermes gateway sandbox; child tool-execution containers " |
| 922 | + "will not show their host/gateway config." |
| 923 | + if platform_text |
| 924 | + else "- Messaging adapters run in the parent Hermes gateway sandbox; child " |
| 925 | + "tool-execution containers will not show their host/gateway config." |
| 926 | + ) |
| 927 | + |
| 928 | + return "\n".join( |
| 929 | + [ |
| 930 | + "NemoClaw runtime context:", |
| 931 | + "- You are Hermes Agent running in a NemoClaw-managed OpenShell sandbox, " |
| 932 | + "not a host-only assistant.", |
| 933 | + "- Some tools, especially managed code/terminal tools, execute in child " |
| 934 | + "tool sandboxes such as Modal. Seeing /__modal, MODAL_SANDBOX_ID, a " |
| 935 | + "missing hermes binary, or missing ~/.hermes-data inside a tool shell " |
| 936 | + "means that shell is a child tool sandbox, not proof that Hermes is " |
| 937 | + "running on the host.", |
| 938 | + f"- Parent Hermes sandbox config lives under {hermes_home} and " |
| 939 | + "/sandbox/.hermes when available. Use nemoclaw_status or " |
| 940 | + "nemoclaw_info for NemoClaw environment questions.", |
| 941 | + f"- NemoClaw provider state: model={info['model']}, " |
| 942 | + f"provider={info['provider']}, endpoint={info['base_url']}, " |
| 943 | + f"gateway={info['gateway']}.", |
| 944 | + "- NemoClaw tools available: nemoclaw_status, nemoclaw_info, " |
| 945 | + "nemoclaw_reload_skills, transcribe_audio.", |
| 946 | + f"- Managed Nous tool broker: {broker_state}; configured services: " |
| 947 | + f"{service_text}. Raw Nous OAuth tokens are host-managed by NemoClaw " |
| 948 | + "and should not be expected inside the sandbox.", |
| 949 | + platform_line, |
| 950 | + ], |
| 951 | + ) |
| 952 | + |
| 953 | + |
| 954 | +def _pre_llm_call(**kwargs): |
| 955 | + """Inject non-visible NemoClaw runtime context into relevant Hermes turns.""" |
| 956 | + if not _should_inject_nemoclaw_context( |
| 957 | + user_message=kwargs.get("user_message"), |
| 958 | + is_first_turn=bool(kwargs.get("is_first_turn")), |
| 959 | + ): |
| 960 | + return None |
| 961 | + _install_nous_tool_broker_patch() |
| 962 | + return {"context": _build_nemoclaw_agent_context(platform=kwargs.get("platform"))} |
| 963 | + |
| 964 | + |
861 | 965 | def _handle_status(tool_input=None, context=None, **_kwargs): |
862 | 966 | """Handle the nemoclaw_status tool call.""" |
863 | 967 | info = _get_sandbox_info() |
@@ -1043,6 +1147,10 @@ def register(ctx): |
1043 | 1147 | description="Reload skills from disk without gateway restart", |
1044 | 1148 | ) |
1045 | 1149 |
|
| 1150 | + # Ground the model quietly through Hermes' context hook. This replaces the |
| 1151 | + # old visible startup banner without reintroducing TUI interrupt noise. |
| 1152 | + ctx.register_hook("pre_llm_call", _pre_llm_call) |
| 1153 | + |
1046 | 1154 | # Refresh skills silently on session start. Earlier versions injected a |
1047 | 1155 | # system banner here, but that can interrupt the user's first prompt in the |
1048 | 1156 | # Hermes TUI because plugin-injected messages travel through Hermes's |
|
0 commit comments