Merge remote-tracking branch 'origin/main' into ethie/pm-clean

This commit is contained in:
ethernet
2026-09-19 23:08:38 -04:00
313 changed files with 11786 additions and 628 deletions

View File

@@ -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
)

View File

@@ -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