Add Hermes Agent package fallback and xAI OAuth (#808)
This commit is contained in:
@@ -22,6 +22,7 @@ export const PROVIDER_ENV_MAP: Record<string, { api_key_env: string; base_url_en
|
||||
'alibaba-coding-plan': { api_key_env: 'ALIBABA_CODING_PLAN_API_KEY', base_url_env: 'ALIBABA_CODING_PLAN_BASE_URL' },
|
||||
anthropic: { api_key_env: 'ANTHROPIC_API_KEY', base_url_env: '' },
|
||||
xai: { api_key_env: 'XAI_API_KEY', base_url_env: '' },
|
||||
'xai-oauth': { api_key_env: '', base_url_env: '' },
|
||||
xiaomi: { api_key_env: 'XIAOMI_API_KEY', base_url_env: '' },
|
||||
'xiaomi-token-plan': { api_key_env: '', base_url_env: '' },
|
||||
gemini: { api_key_env: 'GEMINI_API_KEY', base_url_env: '' },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Agent Bridge
|
||||
|
||||
Optional backend-side bridge for talking to `~/.hermes/hermes-agent` by
|
||||
instantiating `run_agent.AIAgent` directly in a Python process.
|
||||
Optional backend-side bridge for talking to Hermes Agent by instantiating
|
||||
`run_agent.AIAgent` directly in a Python process.
|
||||
|
||||
This is intentionally separate from the current Web UI chat path.
|
||||
|
||||
@@ -37,6 +37,7 @@ The service discovers Hermes Agent in this order:
|
||||
3. the installed `hermes` command path
|
||||
4. current working directory and parent directories
|
||||
5. common locations such as `~/.hermes/hermes-agent`, `~/hermes-agent`, and `/opt/hermes-agent`
|
||||
6. the `hermes-agent` package installed in the selected Python environment
|
||||
|
||||
Hermes home is resolved from `--hermes-home`, `HERMES_HOME`, then `~/.hermes`.
|
||||
|
||||
@@ -54,6 +55,12 @@ python packages/server/src/services/hermes/agent-bridge/hermes_bridge.py \
|
||||
--hermes-home ~/.hermes
|
||||
```
|
||||
|
||||
If no source checkout containing `run_agent.py` is found, the bridge falls back
|
||||
to importing `run_agent` from the Python environment. This supports package
|
||||
installs such as `pip install hermes-agent`. The Node manager prefers the source
|
||||
checkout's virtualenv when a checkout exists, then the Python interpreter from
|
||||
the installed `hermes` command, then the system Python.
|
||||
|
||||
The socket transport uses Python and Node standard libraries. No ZMQ dependency
|
||||
is required.
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import queue
|
||||
@@ -116,10 +117,17 @@ def _candidate_agent_roots(raw: str | None = None) -> list[Path]:
|
||||
return unique
|
||||
|
||||
|
||||
def _discover_agent_root(raw: str | None = None) -> Path:
|
||||
def _find_agent_root(raw: str | None = None) -> Path | None:
|
||||
for candidate in _candidate_agent_roots(raw):
|
||||
if (candidate / "run_agent.py").exists():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def _discover_agent_root(raw: str | None = None) -> Path:
|
||||
root = _find_agent_root(raw)
|
||||
if root is not None:
|
||||
return root
|
||||
attempted = ", ".join(str(path) for path in _candidate_agent_roots(raw))
|
||||
raise RuntimeError(
|
||||
"hermes-agent run_agent.py not found. Pass --agent-root or set "
|
||||
@@ -154,8 +162,8 @@ def _jsonable(value: Any) -> Any:
|
||||
return str(value)
|
||||
|
||||
|
||||
def _agent_root() -> Path:
|
||||
return _discover_agent_root(os.environ.get("HERMES_AGENT_ROOT"))
|
||||
def _agent_root() -> Path | None:
|
||||
return _find_agent_root(os.environ.get("HERMES_AGENT_ROOT"))
|
||||
|
||||
|
||||
def _hermes_home() -> Path:
|
||||
@@ -216,7 +224,11 @@ def _profile_dotenv_keys() -> set[str]:
|
||||
|
||||
|
||||
def _set_path_env(agent_root: str | None = None, hermes_home: str | None = None) -> None:
|
||||
os.environ["HERMES_AGENT_ROOT"] = str(_discover_agent_root(agent_root))
|
||||
resolved_root = _discover_agent_root(agent_root) if agent_root else _find_agent_root()
|
||||
if resolved_root is not None:
|
||||
os.environ["HERMES_AGENT_ROOT"] = str(resolved_root)
|
||||
else:
|
||||
os.environ.pop("HERMES_AGENT_ROOT", None)
|
||||
resolved_home = _discover_hermes_home(hermes_home)
|
||||
os.environ["HERMES_HOME"] = str(resolved_home)
|
||||
os.environ["HERMES_AGENT_BRIDGE_BASE_HOME"] = str(_normalize_base_home(resolved_home))
|
||||
@@ -224,11 +236,16 @@ def _set_path_env(agent_root: str | None = None, hermes_home: str | None = None)
|
||||
|
||||
def _ensure_agent_imports() -> None:
|
||||
root = _agent_root()
|
||||
if not (root / "run_agent.py").exists():
|
||||
raise RuntimeError(f"hermes-agent run_agent.py not found under {root}")
|
||||
root_s = str(root)
|
||||
if root_s not in sys.path:
|
||||
sys.path.insert(0, root_s)
|
||||
if root is not None:
|
||||
root_s = str(root)
|
||||
if root_s not in sys.path:
|
||||
sys.path.insert(0, root_s)
|
||||
elif importlib.util.find_spec("run_agent") is None:
|
||||
raise RuntimeError(
|
||||
"hermes-agent run_agent.py not found in source locations and the "
|
||||
"current Python environment cannot import run_agent. Install "
|
||||
"hermes-agent or pass --agent-root/HERMES_AGENT_ROOT."
|
||||
)
|
||||
os.environ.setdefault("HERMES_HOME", str(_hermes_home()))
|
||||
os.environ.setdefault("HERMES_AGENT_BRIDGE_BASE_HOME", str(_hermes_home()))
|
||||
|
||||
|
||||
@@ -47,6 +47,12 @@ function pathCandidates(agentRoot?: string): string[] {
|
||||
}
|
||||
|
||||
function uvCandidates(agentRoot?: string): string[] {
|
||||
if (!agentRoot) {
|
||||
return [
|
||||
process.env.HERMES_AGENT_BRIDGE_UV,
|
||||
process.env.UV,
|
||||
].filter((value): value is string => !!value && value.trim().length > 0)
|
||||
}
|
||||
return [
|
||||
process.env.HERMES_AGENT_BRIDGE_UV,
|
||||
process.env.UV,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { resolve, join } from 'path'
|
||||
import { homedir } from 'os'
|
||||
import { readFileSync, existsSync, statSync } from 'fs'
|
||||
import yaml from 'js-yaml'
|
||||
import { PROVIDER_PRESETS } from '../../shared/providers'
|
||||
@@ -44,6 +43,7 @@ const MODEL_CACHE_PROVIDER_ALIASES: Record<string, string[]> = {
|
||||
'glm-coding-plan': ['zai-coding-plan'],
|
||||
'kimi-coding': ['kimi-for-coding'],
|
||||
'kimi-coding-cn': ['kimi-for-coding'],
|
||||
'xai-oauth': ['xai'],
|
||||
}
|
||||
|
||||
// --- Config YAML helpers (js-yaml) ---
|
||||
|
||||
@@ -222,15 +222,31 @@ function extractError(err: any): string {
|
||||
export async function listHermesPlugins(): Promise<HermesPluginsResponse> {
|
||||
const command = resolveAgentBridgeCommand()
|
||||
const agentRoot = command.agentRoot || ''
|
||||
const env = {
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
HERMES_AGENT_ROOT_RESOLVED: agentRoot,
|
||||
HERMES_HOME: getActiveProfileDir(),
|
||||
}
|
||||
if (!agentRoot) {
|
||||
delete env.PYTHONHOME
|
||||
delete env.PYTHONPATH
|
||||
}
|
||||
const pythonArgs = [
|
||||
...command.argsPrefix,
|
||||
...(agentRoot ? ['-I'] : []),
|
||||
'-c',
|
||||
PYTHON_BRIDGE,
|
||||
]
|
||||
const displayArgs = [
|
||||
...command.argsPrefix,
|
||||
...(agentRoot ? ['-I'] : []),
|
||||
'-c',
|
||||
'<plugin-discovery>',
|
||||
].join(' ')
|
||||
|
||||
const errors: string[] = []
|
||||
try {
|
||||
const { stdout, stderr } = await execFileAsync(command.command, [...command.argsPrefix, '-I', '-c', PYTHON_BRIDGE], {
|
||||
const { stdout, stderr } = await execFileAsync(command.command, pythonArgs, {
|
||||
cwd: process.cwd(),
|
||||
env,
|
||||
windowsHide: true,
|
||||
@@ -246,8 +262,7 @@ export async function listHermesPlugins(): Promise<HermesPluginsResponse> {
|
||||
}
|
||||
return parsed
|
||||
} catch (err: any) {
|
||||
const args = [...command.argsPrefix, '-I', '-c', '<plugin-discovery>'].join(' ')
|
||||
errors.push(`${command.command} ${args}: ${extractError(err)}`)
|
||||
errors.push(`${command.command} ${displayArgs}: ${extractError(err)}`)
|
||||
}
|
||||
|
||||
throw new Error(`Failed to discover Hermes plugins.\n${errors.join('\n')}`)
|
||||
|
||||
Reference in New Issue
Block a user