Merge remote-tracking branch 'origin/main' into ethie/pm-clean
This commit is contained in:
@@ -211,6 +211,10 @@ def _take_interrupted_prompt(state: SessionState) -> tuple[bool, str]:
|
||||
return True, text
|
||||
|
||||
|
||||
class ModelRejected(ValueError):
|
||||
"""``switch_model`` refused the requested model (no provider can serve it)."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class _TurnCallbacks:
|
||||
"""Per-turn ACP streaming callbacks; all None when no client is connected."""
|
||||
@@ -333,9 +337,8 @@ class HermesACPAgent(SlashCommandsMixin, acp.Agent):
|
||||
user_providers=cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {},
|
||||
custom_providers=get_compatible_custom_providers(cfg))
|
||||
if not result.success:
|
||||
raise ValueError(result.error_message or f"Cannot switch to {raw_model}")
|
||||
raise ModelRejected(result.error_message or f"Cannot switch to {raw_model}")
|
||||
target_provider, new_model = result.target_provider, result.new_model
|
||||
state.model = new_model
|
||||
endpoint: dict[str, Any] = {}
|
||||
if keep_endpoint and not (current_provider and target_provider != current_provider):
|
||||
endpoint = {
|
||||
@@ -343,12 +346,15 @@ class HermesACPAgent(SlashCommandsMixin, acp.Agent):
|
||||
}
|
||||
# ACP-provided MCP servers live only on the running agent's toolsets (``_register_session_mcp_servers``);
|
||||
# a rebuild that re-derived them from config would silently drop every session MCP tool (#42719).
|
||||
state.agent = self.session_manager._make_agent(
|
||||
agent = self.session_manager._make_agent(
|
||||
session_id=state.session_id, cwd=state.cwd, model=new_model,
|
||||
requested_provider=target_provider, **endpoint,
|
||||
enabled_toolsets=getattr(state.agent, "enabled_toolsets", None),
|
||||
disabled_toolsets=getattr(state.agent, "disabled_toolsets", None),
|
||||
)
|
||||
# Assign only after the rebuild succeeded so a failed switch leaves the session on its
|
||||
# working model instead of a model/agent mismatch that persists via save_session.
|
||||
state.agent, state.model = agent, new_model
|
||||
self.session_manager.save_session(state.session_id)
|
||||
return current_provider, target_provider, new_model
|
||||
|
||||
@@ -1002,8 +1008,16 @@ class HermesACPAgent(SlashCommandsMixin, acp.Agent):
|
||||
if state:
|
||||
# switch_model() does synchronous network I/O (models.dev, custom-endpoint probes,
|
||||
# ~10 s cold) — off the loop, like the gateway, so other ACP sessions keep flowing.
|
||||
_old, requested_provider, resolved_model = await asyncio.to_thread(
|
||||
self._switch_model, state, model_id, keep_endpoint=True)
|
||||
try:
|
||||
_old, requested_provider, resolved_model = await asyncio.to_thread(
|
||||
self._switch_model, state, model_id, keep_endpoint=True)
|
||||
except ModelRejected as exc:
|
||||
# A model no provider can serve is a bad ``modelId`` param (-32602), not an agent
|
||||
# internal error (-32603): the client attributes it to the request, not to Hermes (#72439).
|
||||
# Only the switch_model rejection maps here; a ValueError from the rebuild itself
|
||||
# (disabled provider, context window below the floor) stays on the -32603 path.
|
||||
from acp.exceptions import RequestError
|
||||
raise RequestError.invalid_params({"details": str(exc)}) from exc
|
||||
logger.info(
|
||||
"Session %s: model switched to %s via provider %s", session_id, resolved_model, requested_provider
|
||||
)
|
||||
|
||||
@@ -417,6 +417,7 @@ class SessionManager:
|
||||
# models). Resolved against the session's model so per-model overrides apply.
|
||||
"reasoning_config": resolve_reasoning_config(config, model or default_model),
|
||||
}
|
||||
resolve_error: Exception | None = None
|
||||
try:
|
||||
runtime = resolve_runtime_provider(
|
||||
requested=requested_provider or config_provider, target_model=(model or default_model) or None)
|
||||
@@ -426,7 +427,8 @@ class SessionManager:
|
||||
"credential_pool": runtime.get("credential_pool"),
|
||||
"command": runtime.get("command"), "args": list(runtime.get("args") or []),
|
||||
})
|
||||
except Exception:
|
||||
except Exception as exc:
|
||||
resolve_error = exc
|
||||
logger.debug("ACP session falling back to default provider resolution", exc_info=True)
|
||||
|
||||
_register_task_cwd(session_id, cwd)
|
||||
@@ -444,7 +446,15 @@ class SessionManager:
|
||||
except Exception:
|
||||
logger.debug("ACP: bounded MCP discovery wait failed", exc_info=True)
|
||||
|
||||
agent = AIAgent(**kwargs)
|
||||
try:
|
||||
agent = AIAgent(**kwargs)
|
||||
except Exception as exc:
|
||||
# The bare-AIAgent fallback dies with "No LLM provider configured. Run `hermes setup`" on a
|
||||
# machine that is configured and was working a call earlier; the swallowed resolution
|
||||
# failure (revoked OAuth, disabled provider, ...) is the actionable error (#91090).
|
||||
if resolve_error is not None:
|
||||
raise resolve_error from exc
|
||||
raise
|
||||
# ACP stdio: stdout is protocol-only JSON-RPC; agent chatter goes to stderr.
|
||||
agent._print_fn = _acp_stderr_print
|
||||
return agent
|
||||
|
||||
Reference in New Issue
Block a user