Compare commits

..
Author SHA1 Message Date
Teknium 372e94b49a fix(errors): classify throttle messages before token-overflow patterns; add new overflow shapes
Port from anomalyco/opencode#37848 (+ dev-branch twin #37840): expand
context-overflow patterns and guard against rate-limit messages that
mention tokens.

- 'Throttling error: Too many tokens, please wait before trying again.'
  (AWS Bedrock / proxy shape) classified as context_overflow and routed a
  healthy session into compression on every throttle. Added 'throttling'
  to _RATE_LIMIT_PATTERNS, which the message-only path checks BEFORE the
  overflow list.
- 'Input length N exceeds the maximum allowed input length of M tokens.'
  (Together/Fireworks shape) fell through to unknown — no compression
  recovery. Added 'maximum allowed input length' to overflow patterns.
- 'request_too_large' / 'Request exceeds the maximum size' (Anthropic 413
  type re-wrapped without a status code by aggregators/proxies) fell
  through to unknown. Added to _PAYLOAD_TOO_LARGE_PATTERNS.

All three shapes proven live on main before the fix; 265 classifier +
bedrock tests and 238 sibling rate-guard/compression tests pass.
2026-07-23 17:10:32 -07:00
316 changed files with 3165 additions and 9585 deletions
+40 -48
View File
@@ -69,43 +69,6 @@ def _ra():
return run_agent
def _moa_reference_output_allowed(agent: Any) -> bool:
"""Keep MoA display events off only the machine-readable ``-Q`` surface."""
return not (
getattr(agent, "platform", None) == "cli"
and getattr(agent, "tool_progress_mode", "all") == "off"
)
def _relay_moa_reference_event(agent: Any, event: str, **kwargs: Any) -> None:
"""Relay MoA display events while preserving the ``-Q`` stdout contract."""
if not _moa_reference_output_allowed(agent):
return
cb = getattr(agent, "tool_progress_callback", None)
if cb is None:
return
try:
if event == "moa.reference":
cb(
"moa.reference",
str(kwargs.get("label") or ""),
str(kwargs.get("text") or ""),
None,
moa_index=kwargs.get("index"),
moa_count=kwargs.get("count"),
)
elif event == "moa.aggregating":
cb(
"moa.aggregating",
str(kwargs.get("aggregator") or ""),
None,
None,
moa_ref_count=kwargs.get("ref_count"),
)
except Exception:
pass
def _normalize_route_base_url(base_url: Any) -> str:
"""Canonicalize an endpoint URL for model-route identity comparisons."""
return normalize_route_base_url(base_url)
@@ -1062,20 +1025,49 @@ def init_agent(
elif isinstance(effective_key, str) and len(effective_key) > 12:
print(f"🔑 Using token: {effective_key[:8]}...{effective_key[-4:]}")
elif agent.provider == "moa":
from agent.moa_loop import build_moa_facade
from agent.moa_loop import MoAClient
agent.api_mode = "chat_completions"
# build_moa_facade wires the reference relay that routes
# reference-model outputs to the agent's tool_progress_callback so
# Route reference-model outputs to the agent's tool_progress_callback so
# every surface that already consumes it (CLI spinner/scrollback, TUI,
# desktop, gateway) can show each reference's answer as a labelled
# block before the aggregator acts. The facade emits "moa.reference",
# "moa.progress", "moa.phase", and "moa.aggregating" events, forwarded
# through the same callback the tool lifecycle uses. Best-effort and
# cache-safe — display-only events, they never touch the message
# history. The factory is shared with the fallback-restore/recovery
# paths so a restored facade keeps emitting these events (#53802).
agent.client = build_moa_facade(agent, agent.model)
# desktop, gateway) can show each reference's answer as a labelled block
# before the aggregator acts. The facade emits "moa.reference" and
# "moa.aggregating" events; we forward them through the same callback
# the tool lifecycle uses. Best-effort and cache-safe — these are
# display-only events, they never touch the message history.
def _moa_reference_relay(event: str, **kwargs: Any) -> None:
cb = getattr(agent, "tool_progress_callback", None)
if cb is None:
return
try:
if event == "moa.reference":
label = str(kwargs.get("label") or "")
text = str(kwargs.get("text") or "")
idx = kwargs.get("index")
count = kwargs.get("count")
cb(
"moa.reference",
label,
text,
None,
moa_index=idx,
moa_count=count,
)
elif event == "moa.aggregating":
cb(
"moa.aggregating",
str(kwargs.get("aggregator") or ""),
None,
None,
moa_ref_count=kwargs.get("ref_count"),
)
except Exception:
pass
agent.client = MoAClient(
agent.model or "default",
reference_callback=_moa_reference_relay,
)
agent._client_kwargs = {}
agent.api_key = api_key or "moa-virtual-provider"
agent.base_url = "moa://local"
+3 -22
View File
@@ -1225,14 +1225,6 @@ def try_recover_primary_transport(
)
agent._is_anthropic_oauth = rt["is_anthropic_oauth"]
agent.client = None
elif (agent.provider or "").strip().lower() == "moa":
# MoA is a virtual provider with empty client_kwargs — rebuilding
# via _create_openai_client would raise "api_key client option
# must be set". Recreate the facade through the shared factory so
# the reference_callback relay survives recovery (#53802).
from agent.moa_loop import build_moa_facade
agent.client = build_moa_facade(agent, agent.model)
else:
agent.client = agent._create_openai_client(
dict(rt["client_kwargs"]),
@@ -1396,18 +1388,7 @@ def restore_primary_runtime(agent) -> bool:
)
# ── Rebuild client for the primary provider ──
if agent.provider == "moa":
# MoA is a virtual chat-completions provider. It never has real
# OpenAI client kwargs; restoring it after a fallback must recreate
# the facade, not call OpenAI() with an empty api_key. Use the
# shared factory so the restored facade keeps the reference_callback
# relay wired at init — a bare MoAClient() would silently stop
# emitting moa.reference/moa.aggregating display events (#53802).
from agent.moa_loop import build_moa_facade
agent.client = build_moa_facade(agent, agent.model)
agent._anthropic_client = None
elif agent.api_mode == "anthropic_messages":
if agent.api_mode == "anthropic_messages":
from agent.anthropic_adapter import build_anthropic_client
agent._anthropic_api_key = rt["anthropic_api_key"]
agent._anthropic_base_url = rt["anthropic_base_url"]
@@ -2143,7 +2124,7 @@ def switch_model(agent, new_model, new_provider, api_key='', base_url='', api_mo
# ── Build new client ──
if (new_provider or "").strip().lower() == "moa":
from agent.moa_loop import build_moa_facade
from agent.moa_loop import MoAClient
# The MoA virtual provider speaks only chat.completions via the
# MoAClient facade — the aggregator's real transport
@@ -2160,7 +2141,7 @@ def switch_model(agent, new_model, new_provider, api_key='', base_url='', api_mo
agent.api_key = api_key or "moa-virtual-provider"
agent.base_url = "moa://local"
agent._client_kwargs = {}
agent.client = build_moa_facade(agent, agent.model)
agent.client = MoAClient(agent.model or "default")
elif api_mode == "anthropic_messages":
from agent.anthropic_adapter import (
build_anthropic_client,
-19
View File
@@ -3648,7 +3648,6 @@ def _retry_same_provider_sync(
effective_timeout: float,
effective_extra_body: dict,
reasoning_config: Optional[dict],
extra_headers: Optional[Dict[str, str]] = None,
) -> Any:
if task == "vision":
_, retry_client, retry_model = resolve_vision_provider_client(
@@ -3686,11 +3685,6 @@ def _retry_same_provider_sync(
base_url=retry_base or resolved_base_url,
task=task,
)
# Preserve per-request attribution headers (e.g. Copilot's
# ``x-initiator: user``) across the rebuilt-client retry — dropping them
# here would let a recovery retry silently lose capability gating (#60293).
if extra_headers:
retry_kwargs["extra_headers"] = dict(extra_headers)
if _is_anthropic_compat_endpoint(resolved_provider, retry_base):
retry_kwargs["messages"] = _convert_openai_images_to_anthropic(retry_kwargs["messages"])
return _validate_llm_response(
@@ -3714,7 +3708,6 @@ async def _retry_same_provider_async(
effective_timeout: float,
effective_extra_body: dict,
reasoning_config: Optional[dict],
extra_headers: Optional[Dict[str, str]] = None,
) -> Any:
if task == "vision":
_, retry_client, retry_model = resolve_vision_provider_client(
@@ -3752,10 +3745,6 @@ async def _retry_same_provider_async(
base_url=retry_base or resolved_base_url,
task=task,
)
# Preserve per-request attribution headers across the rebuilt-client
# retry — see the sync variant above (#60293).
if extra_headers:
retry_kwargs["extra_headers"] = dict(extra_headers)
if _is_anthropic_compat_endpoint(resolved_provider, retry_base):
retry_kwargs["messages"] = _convert_openai_images_to_anthropic(retry_kwargs["messages"])
return _validate_llm_response(
@@ -7119,7 +7108,6 @@ def call_llm(
timeout: float = None,
extra_body: dict = None,
reasoning_config: Optional[dict] = None,
extra_headers: Optional[Dict[str, str]] = None,
api_mode: str = None,
stream: bool = False,
stream_options: dict = None,
@@ -7145,9 +7133,6 @@ def call_llm(
extra_body: Additional request body fields.
reasoning_config: Optional Hermes reasoning config for direct model calls
such as MoA reference/aggregator slots.
extra_headers: Additional per-request HTTP headers. These override
client-level defaults for providers that gate capabilities on
request attribution (for example Copilot's ``x-initiator``).
stream: When True, return the raw SDK streaming iterator instead of a
validated complete response. The caller is responsible for consuming
chunks (and for any fallback). Used by the MoA aggregator so its
@@ -7261,8 +7246,6 @@ def call_llm(
tools=tools, timeout=effective_timeout, extra_body=effective_extra_body,
reasoning_config=reasoning_config,
base_url=_base_info or resolved_base_url, task=task)
if extra_headers:
kwargs["extra_headers"] = dict(extra_headers)
# Convert image blocks for Anthropic-compatible endpoints (e.g. MiniMax)
_client_base = str(getattr(client, "base_url", "") or "")
@@ -7516,7 +7499,6 @@ def call_llm(
effective_timeout=effective_timeout,
effective_extra_body=effective_extra_body,
reasoning_config=reasoning_config,
extra_headers=extra_headers,
)
# ── Same-provider credential-pool recovery ─────────────────────
@@ -7560,7 +7542,6 @@ def call_llm(
effective_timeout=effective_timeout,
effective_extra_body=effective_extra_body,
reasoning_config=reasoning_config,
extra_headers=extra_headers,
)
except Exception as retry2_err:
# The rotated key also hit a quota/auth wall. Mark it
+5 -69
View File
@@ -1946,17 +1946,7 @@ def handle_max_iterations(agent, messages: list, api_call_count: int) -> str:
for internal_key in [k for k in api_msg if isinstance(k, str) and k.startswith("_")]:
api_msg.pop(internal_key, None)
if _needs_sanitize:
# In MoA mode, agent.model is the virtual preset name,
# not the actual aggregator model. Resolve the real
# aggregator model so Gemini preserves thought_signature.
_sanitize_model = agent.model
if agent.provider == "moa":
_moa_client = getattr(agent, "client", None)
if _moa_client is not None:
_agg_slot = getattr(_moa_client, "last_aggregator_slot", None)
if _agg_slot and _agg_slot.get("model"):
_sanitize_model = _agg_slot["model"]
agent._sanitize_tool_calls_for_strict_api(api_msg, model=_sanitize_model)
agent._sanitize_tool_calls_for_strict_api(api_msg, model=agent.model)
api_messages.append(api_msg)
effective_system = agent._cached_system_prompt or ""
@@ -2473,11 +2463,7 @@ def interruptible_streaming_api_call(agent, api_kwargs: dict, *, on_first_delta=
request_client_holder = {"client": None, "diag": None, "owner_tid": None}
# Transport kind of the registered request client — see the non-streaming
# variant. Routes _close_request_client_once to anthropic vs openai abort/
# close helpers (#67142). ``kind="stream"`` registers a per-request
# *stream handle* instead of a client — used under the MoA facade, whose
# singleton client has no per-request sockets to abort
# (_abort_request_openai_client is a no-op on it), so interrupts must
# close the stream object itself (#57354).
# close helpers (#67142).
request_client_kind = {"value": "openai"}
request_client_lock = threading.Lock()
# Request-local cancellation flag — see interruptible_api_call for the full
@@ -2497,44 +2483,6 @@ def interruptible_streaming_api_call(agent, api_kwargs: dict, *, on_first_delta=
request_client_holder["owner_tid"] = threading.get_ident()
return client
def _stream_close_callable(stream):
close = getattr(stream, "close", None)
if callable(close):
return close
response = getattr(stream, "response", None)
close = getattr(response, "close", None)
if callable(close):
return close
return None
def _set_request_stream_handle(stream):
# Register the per-request *stream* under kind="stream" so an
# interrupt closes the stream handle itself. Under the MoA facade the
# registered "client" is the shared facade singleton whose
# per-request abort helpers are no-ops, leaving the underlying HTTP
# stream open until the provider drained it (#57354).
if _stream_close_callable(stream) is None:
return stream
with request_client_lock:
request_client_holder["client"] = stream
request_client_kind["value"] = "stream"
request_client_holder["owner_tid"] = threading.get_ident()
return stream
def _close_request_stream_handle(stream, reason: str) -> None:
close = _stream_close_callable(stream)
if close is None:
return
try:
close()
logger.info("Streaming response handle closed (%s)", reason)
except Exception as exc:
logger.debug(
"Streaming response handle close failed (%s): %s",
reason,
exc,
)
def _close_request_client_once(reason: str) -> None:
# See #29507 explanation in the non-streaming variant above. A
# stranger thread (the interrupt-check / stale-stream detector loop)
@@ -2542,15 +2490,9 @@ def interruptible_streaming_api_call(agent, api_kwargs: dict, *, on_first_delta=
# so the worker thread retains ownership of the FD release.
with request_client_lock:
request_client = request_client_holder.get("client")
request_kind = request_client_kind.get("value", "openai")
owner_tid = request_client_holder.get("owner_tid")
# A registered stream handle (kind="stream", MoA facade path) is
# safe to close from any thread — closing IS the abort — so the
# stranger-thread ownership carve-out only applies to real
# per-request clients (#57354).
stranger_thread = (
request_kind != "stream"
and request_client is not None
request_client is not None
and owner_tid is not None
and owner_tid != threading.get_ident()
)
@@ -2559,9 +2501,8 @@ def interruptible_streaming_api_call(agent, api_kwargs: dict, *, on_first_delta=
request_client_holder["owner_tid"] = None
if request_client is None:
return
if request_kind == "stream":
_close_request_stream_handle(request_client, reason)
elif request_kind == "anthropic_messages":
kind = request_client_kind.get("value", "openai")
if kind == "anthropic_messages":
if stranger_thread:
agent._abort_request_anthropic_client(request_client, reason=reason)
else:
@@ -2740,11 +2681,6 @@ def interruptible_streaming_api_call(agent, api_kwargs: dict, *, on_first_delta=
_diag = agent._stream_diag_init()
request_client_holder["diag"] = _diag
stream = request_client.chat.completions.create(**stream_kwargs)
if agent.provider == "moa":
# The MoA facade is a shared singleton — abort/close of the
# registered client is a no-op, so register the stream handle
# itself for interrupt teardown (#57354).
stream = _set_request_stream_handle(stream)
# Claim the delta sink for THIS attempt (#65991). If a prior attempt's
# stream is somehow still alive (a stale-stream reconnect whose socket
# abort raced), this claim supersedes it so its late chunks are fenced
-117
View File
@@ -210,123 +210,6 @@ class ContextEngine(ABC):
"""
return messages, 0
# -- Optional: per-turn context selection (distinct from compression) --
def select_context(
self,
request_messages: List[Dict[str, Any]],
*,
conversation_messages: List[Dict[str, Any]] = None,
incoming_message: Dict[str, Any] = None,
budget_tokens: int = 0,
) -> List[Dict[str, Any]]:
"""Optionally choose/replace the context for THIS request, pre-generation.
Called every turn after the request message list is assembled and
before it is dispatched to the provider — independent of
``should_compress()``. This lets an engine *select* which context
enters the prompt (retrieval, topic routing, role/branch switching)
rather than *shrink* context that is already there. The two verbs are
orthogonal:
- ``compress()`` : context is too long -> make it shorter.
- ``select_context()``: this turn belongs to a different context
-> use that one instead.
Without this hook, engines that need per-turn access to the message
list have to force ``should_compress()`` to return ``True`` so that
``compress()`` is invoked every turn purely as a callback — which
conflates selection with compression and degrades behaviour when the
engine's backend is unavailable. ``select_context()`` removes the need
for that workaround.
The returned list is request-only: it replaces the messages sent to
the provider for this single call and MUST NOT be treated as persisted
transcript state. The conversation history in the session DB is left
untouched, so nothing leaks across turns. Return ``None`` to leave the
request unchanged.
Unlike the ``pre_llm_call`` plugin hook (which appends to the user
message and intentionally never rewrites the list, to preserve the
cache prefix), ``select_context()`` may *replace* the message list.
Ordering / cache contract: the host runs this hook **before** prompt
cache-control and **before** every request sanitizer (orphaned-tool
cleanup, thinking-only/role normalization, whitespace/JSON
normalization). So (a) whatever the hook returns still passes through
the same validation as any request — a malformed replacement cannot
reach the provider — and (b) prompt-cache stability (an AGENTS.md
invariant) is preserved: the default no-op leaves the request
byte-identical, so cache behaviour is unchanged for the built-in
compressor and any non-implementing engine. An engine that *does*
replace the list changes its own cache prefix by definition; that is
the engine's concern, and cache-control breakpoints are re-derived on
the selected list. The hook is evaluated per provider request (so it
re-runs on retries within a turn), consistent with "select the context
for THIS request".
Args:
request_messages: The assembled request message list (system
prompt + history + any ephemeral prefill), in OpenAI format.
conversation_messages: The unmodified persisted conversation
history, for reference only (do not mutate).
incoming_message: The current turn's user message, if available.
budget_tokens: The active model's context length, or 0 if unknown.
Default returns ``None`` (no-op) — zero impact on the built-in
compressor or any existing engine.
"""
return None
def on_turn_complete(
self,
messages: List[Dict[str, Any]],
usage: Dict[str, Any] = None,
**kwargs: Any,
) -> None:
"""Observe a finished user turn (post-turn ingestion / observation).
Called from the standard turn-finalization path once the assistant/tool
loop completes, with the finalized in-memory transcript snapshot. This
is the complement to ``select_context()``: selection happens *before*
the request, while observation happens *after* the turn. It lets an
engine ingest, index, summarize, or update routing / topic / session
state from what actually happened — so the next ``select_context()``
can act on it.
Coverage: this fires from the normal finalization seam. Some abnormal
early-return paths in the loop (e.g. a content-policy block or a
provider terminal failure) persist and return without routing through
finalization, and therefore do not currently emit this hook. Treat it
as a best-effort post-turn observation for completed turns, not a
guaranteed callback for every possible early exit; unifying all
terminal paths behind one finalization seam is a separate follow-up.
Together the two hooks remove the need to abuse ``should_compress()`` /
``compress()`` as a generic per-turn callback just to observe history,
and they cover the case where a turn finishes and there may be no next
request from which to infer the previous turn.
``messages`` is a shallow copy and should be treated as read-only:
return values are ignored and this hook must not rely on transcript
mutation for persistence. ``kwargs`` may include ``turn_id``,
``task_id``, ``api_call_count``, ``interrupted``, ``failed``, and
``turn_exit_reason``.
``usage`` carries the completed turn's canonical token usage (the same
dict shape passed to ``update_from_response`` — ``prompt_tokens`` /
``completion_tokens`` / ``total_tokens`` plus the canonical
``input_tokens`` / ``output_tokens`` / ``cache_read_tokens`` /
``cache_write_tokens`` / ``reasoning_tokens`` buckets) so an engine can
weigh how large/expensive the selected context actually was when
deciding the next ``select_context()``. It is ``None`` on finalized
turns that never reached a provider response (e.g. interrupt); engines
must treat it as optional.
Default is a no-op.
"""
return None
# -- Optional: pre-flight check ----------------------------------------
def should_compress_preflight(self, messages: List[Dict[str, Any]]) -> bool:
+1 -198
View File
@@ -716,136 +716,6 @@ def _sync_failover_system_message(agent, api_messages, active_system_prompt):
return sp
def _apply_context_engine_selection(
agent: Any,
api_messages: List[Dict[str, Any]],
conversation_messages: List[Dict[str, Any]],
incoming_message: Optional[Dict[str, Any]],
*,
logger: Any,
) -> List[Dict[str, Any]]:
"""Run the optional per-turn ``ContextEngine.select_context()`` hook.
Returns the (possibly replaced) request message list. The hook is for
context *selection / routing* (retrieval, topic routing, role switching),
which is distinct from compression and fires every turn independent of
``should_compress()``.
Fail-open by design: a missing hook, any exception, or an invalid return
value yields the unmodified ``api_messages``. The result is request-only —
persisted conversation history is never mutated here.
"""
engine = getattr(agent, "context_compressor", None)
if engine is None or not hasattr(engine, "select_context"):
return api_messages
# Skip the no-op base implementation so non-implementing engines —
# including the built-in ContextCompressor — pay nothing per request:
# no history copies below, no call. ``hasattr`` alone is not enough,
# because the ABC defines a default ``select_context`` that every engine
# inherits. Mirrors the base-method short-circuit in
# ``_notify_context_engine_turn_complete``. Lazy import avoids any import
# cycle with agent.context_engine.
try:
from agent.context_engine import ContextEngine as _CE
if getattr(engine.select_context, "__func__", None) is _CE.select_context:
return api_messages
except Exception:
pass
session_label = getattr(agent, "session_id", None) or "-"
# Pass shallow copies of the reference-only inputs so an engine that
# mutates them in place cannot alter persisted transcript state. Only
# ``request_messages`` (the per-call request list) is meant to be acted on,
# and it may be replaced wholesale via the return value — never mutated in
# place either. ``conversation_messages`` / ``incoming_message`` are
# read-only context; copying enforces the request-only contract rather than
# merely documenting it.
_conv_copy = [dict(m) if isinstance(m, dict) else m for m in conversation_messages] \
if conversation_messages is not None else None
_incoming_copy = dict(incoming_message) if isinstance(incoming_message, dict) else incoming_message
try:
selected = engine.select_context(
api_messages,
conversation_messages=_conv_copy,
incoming_message=_incoming_copy,
budget_tokens=getattr(engine, "context_length", 0) or 0,
)
except Exception:
logger.warning(
"Context engine select_context hook failed; using unmodified "
"request messages (session=%s)",
session_label,
exc_info=True,
)
return api_messages
if selected is None:
return api_messages
# Require a NON-EMPTY list of dicts. An empty list must fall open to the
# original request: ``all([])`` is ``True``, so without the emptiness check
# a ``[]`` returned by a buggy/failing engine would replace a valid request
# with an empty message list that the downstream sanitizers cannot restore,
# reaching the provider as an invalid request instead of failing open.
if isinstance(selected, list) and selected and all(isinstance(m, dict) for m in selected):
return selected
logger.warning(
"Context engine select_context returned an invalid value "
"(not a non-empty list of dicts); ignoring (session=%s)",
session_label,
)
return api_messages
def _notify_context_engine_turn_complete(
agent: Any,
messages: List[Dict[str, Any]],
*,
usage: Optional[Dict[str, Any]] = None,
logger: Any,
**meta: Any,
) -> None:
"""Notify the active context engine that a user turn has finished.
Calls the optional ``ContextEngine.on_turn_complete()`` observation hook
once per turn, after the assistant/tool loop has produced the finalized
transcript. The complement to ``select_context()`` (pre-request selection):
this lets an engine ingest / index / summarize the completed turn.
Fail-open: a missing or no-op hook, or any exception, is swallowed.
``messages`` is passed as a shallow copy so the engine cannot mutate the
persisted transcript.
"""
engine = getattr(agent, "context_compressor", None)
hook = getattr(engine, "on_turn_complete", None)
if engine is None or not callable(hook):
return
# Skip the no-op base implementation so non-implementing engines (incl.
# the built-in compressor) pay nothing per turn. Lazy import avoids any
# import cycle with agent.context_engine.
try:
from agent.context_engine import ContextEngine as _CE
if getattr(hook, "__func__", None) is _CE.on_turn_complete:
return
except Exception:
pass
try:
hook(
[dict(m) if isinstance(m, dict) else m for m in messages],
usage=usage,
**meta,
)
except Exception:
logger.warning(
"Context engine on_turn_complete hook failed (session=%s)",
getattr(agent, "session_id", None) or "-",
exc_info=True,
)
def run_conversation(
agent,
user_message: Any,
@@ -985,13 +855,6 @@ def run_conversation(
# over instead of spinning. Reset here so each turn starts fresh. See #26080.
agent._auth_pool_refresh_counts = {}
# Reset the per-turn usage holder forwarded to the context engine's
# on_turn_complete() observation hook. Set after each successful provider
# response (see below); left as None on turns that never reach a response
# (early failure / interrupt) so the hook receives None rather than a
# stale prior turn's usage.
agent._last_turn_usage = None
# Optional opt-in runtime: if api_mode == codex_app_server, hand the
# turn to the codex app-server subprocess (terminal/file ops/patching
# all run inside Codex). Default Hermes path is bypassed entirely.
@@ -1240,28 +1103,7 @@ def run_conversation(
# Uses new dicts so the internal messages list retains the fields
# for Codex Responses compatibility.
if agent._should_sanitize_tool_calls():
# In MoA mode, agent.model is the virtual preset name
# (e.g. "closed"), not the actual aggregator model. Use
# the resolved aggregator model so Gemini aggregators
# correctly preserve thought_signature (extra_content).
_sanitize_model = agent.model
if agent.provider == "moa":
if moa_config:
_agg = moa_config.get("aggregator") or {}
if _agg.get("model"):
_sanitize_model = _agg["model"]
if _sanitize_model == agent.model:
# Virtual-provider mode: no moa_config is threaded
# through run_conversation — the facade resolves the
# preset internally. Ask the facade for the resolved
# aggregator slot from the previous create() instead
# (set before any history replay that could carry
# thought_signature).
_moa_client = getattr(agent, "client", None)
_agg_slot = getattr(_moa_client, "last_aggregator_slot", None)
if _agg_slot and _agg_slot.get("model"):
_sanitize_model = _agg_slot["model"]
agent._sanitize_tool_calls_for_strict_api(api_msg, model=_sanitize_model)
agent._sanitize_tool_calls_for_strict_api(api_msg, model=agent.model)
# Keep 'reasoning_details' - OpenRouter uses this for multi-turn reasoning context
# The signature field helps maintain reasoning continuity
api_messages.append(api_msg)
@@ -1308,17 +1150,6 @@ def run_conversation(
temperature=_preset_temperature(moa_config, "reference_temperature"),
aggregator_temperature=_preset_temperature(moa_config, "aggregator_temperature"),
reference_max_tokens=moa_config.get("reference_max_tokens"),
# None = no per-preset override; inherit
# auxiliary.moa_reference.timeout via call_llm.
reference_timeout=(
float(moa_config["reference_timeout"])
if moa_config.get("reference_timeout")
else None
),
degraded_reference_policy=str(
moa_config.get("degraded_reference_policy") or "loud"
),
agent=agent,
)
if _moa_context:
for _msg in reversed(api_messages):
@@ -1345,26 +1176,6 @@ def run_conversation(
for idx, pfm in enumerate(agent.prefill_messages):
api_messages.insert(sys_offset + idx, pfm.copy())
# Per-turn context selection hook (additive, no-op by default).
# Lets a context engine select/replace which context enters the
# prompt for THIS call only — retrieval, topic routing, role/branch
# switching — distinct from compression and independent of
# should_compress(). Request-only: persisted history is untouched, so
# caching/sanitization below operate on whatever the engine selected.
# Fail-open (see _apply_context_engine_selection).
_sel_incoming = (
messages[current_turn_user_idx]
if 0 <= current_turn_user_idx < len(messages)
else None
)
api_messages = _apply_context_engine_selection(
agent,
api_messages,
messages,
_sel_incoming,
logger=request_logger,
)
# Apply Anthropic prompt caching for Claude models on native
# Anthropic, OpenRouter, and third-party Anthropic-compatible
# gateways. Auto-detected: if ``_use_prompt_caching`` is set,
@@ -2777,14 +2588,6 @@ def run_conversation(
"reasoning_tokens": canonical_usage.reasoning_tokens,
}
agent.context_compressor.update_from_response(usage_dict)
# Stash this response's canonical usage so the post-turn
# on_turn_complete() observation hook can forward it (the
# same dict shape passed to update_from_response). A turn
# may make several API calls; the engine's per-turn signal
# of interest is the cost/size of the latest assembled
# request, so we keep the most recent call's usage.
agent._last_turn_usage = dict(usage_dict)
elif getattr(
agent.context_compressor,
"awaiting_real_usage_after_compression",
+1 -3
View File
@@ -422,9 +422,7 @@ CURATOR_REVIEW_PROMPT = (
"INSTRUCTIONS AND EXPERIENTIAL KNOWLEDGE. A collection of hundreds of "
"narrow skills where each one captures one session's specific bug is "
"a FAILURE of the library — not a feature. An agent searching skills "
"matches on descriptions, not on exact names (note: long descriptions "
"are truncated to 57 chars in the system prompt skill index — keep the "
"trigger class in that window). One broad umbrella "
"matches on descriptions, not on exact names; one broad umbrella "
"skill with labeled subsections beats five narrow siblings for "
"discoverability, not the other way around.\n\n"
"The right target shape is CLASS-LEVEL skills with rich SKILL.md "
+18
View File
@@ -159,6 +159,14 @@ _RATE_LIMIT_PATTERNS = [
"throttlingexception",
"too many concurrent requests",
"servicequotaexceededexception",
# Generic throttle prefix — Bedrock (and some proxies) surface throttling
# as "Throttling error: Too many tokens, please wait before trying
# again." Without this entry the message falls through to the
# context-overflow list (which contains "too many tokens") and the retry
# loop compresses a healthy session instead of backing off. Matched
# BEFORE _CONTEXT_OVERFLOW_PATTERNS in the message-only path, so the
# throttle wins. (port of anomalyco/opencode#37848's exclusion guard)
"throttling",
]
# Patterns that indicate provider-side overload, NOT a per-credential rate
@@ -212,6 +220,12 @@ _PAYLOAD_TOO_LARGE_PATTERNS = [
"request entity too large",
"payload too large",
"error code: 413",
# Anthropic's structured 413 error type. Normally arrives with an HTTP
# 413 status (handled by the status path), but aggregators/proxies can
# re-wrap it into a plain message with no status attribute — route it to
# the same compression recovery. (port of anomalyco/opencode#37848)
"request_too_large",
"request exceeds the maximum size",
]
# Image-size patterns. Matched against 400 bodies (not 413) because most
@@ -298,6 +312,10 @@ _CONTEXT_OVERFLOW_PATTERNS = [
"max input token",
"input token",
"exceeds the maximum number of input tokens",
# Together/Fireworks-style: "Input length 131393 exceeds the maximum
# allowed input length of 131040 tokens." No other pattern in this list
# matches that wording. (port of anomalyco/opencode#37848)
"maximum allowed input length",
]
# Model not found patterns
+2 -6
View File
@@ -270,12 +270,8 @@ def _translate_tool_call_to_gemini(tool_call: Dict[str, Any]) -> Dict[str, Any]:
}
}
thought_signature = _tool_call_extra_signature(tool_call)
# Fallback sentinel for cross-provider tool_calls (e.g. fallback from
# xAI/Anthropic to Gemini, where the original tool_call carries no
# Gemini thoughtSignature). Mirrors gemini_cloudcode_adapter.py:106.
# Without this, Gemini 3 thinking models reject replayed history with
# 400 INVALID_ARGUMENT on the missing thoughtSignature.
part["thoughtSignature"] = thought_signature or "skip_thought_signature_validator"
if thought_signature:
part["thoughtSignature"] = thought_signature
return part
-9
View File
@@ -56,8 +56,6 @@ from pathlib import Path
from typing import Any, Awaitable, Callable, Dict, List, Optional, Set
from urllib.parse import quote, unquote
from hermes_cli._subprocess_compat import windows_hide_flags
from agent.lsp.protocol import (
ERROR_CONTENT_MODIFIED,
ERROR_METHOD_NOT_FOUND,
@@ -296,12 +294,6 @@ class LSPClient:
cmd = self._command
if sys.platform == "win32":
cmd = self._win_wrap_cmd(cmd)
# Suppress the cmd.exe console window that would otherwise flash
# every time we launch a ``.cmd``-wrapped language server
# (e.g. pyright-langserver.CMD) from a console-less host such as
# a VS Code/Zed extension running the ACP adapter.
# windows_hide_flags() is CREATE_NO_WINDOW on Windows, 0 on POSIX.
creationflags = windows_hide_flags()
try:
# start_new_session=True detaches the LSP server into its own
@@ -320,7 +312,6 @@ class LSPClient:
env=env,
cwd=self._cwd,
start_new_session=True,
creationflags=creationflags,
)
except FileNotFoundError as e:
raise LSPProtocolError(
-4
View File
@@ -35,8 +35,6 @@ import threading
from pathlib import Path
from typing import Any, Dict, Optional
from hermes_cli._subprocess_compat import windows_hide_flags
logger = logging.getLogger("agent.lsp.install")
# Package-name → install-strategy hint registry. Each entry is a
@@ -270,7 +268,6 @@ def _install_npm(
text=True,
timeout=300,
stdin=subprocess.DEVNULL,
creationflags=windows_hide_flags(),
)
if proc.returncode != 0:
logger.warning(
@@ -320,7 +317,6 @@ def _install_go(pkg: str, bin_name: str) -> Optional[str]:
timeout=600,
env=env,
stdin=subprocess.DEVNULL,
creationflags=windows_hide_flags(),
)
if proc.returncode != 0:
logger.warning(
+60 -915
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -453,8 +453,8 @@ def reload_skills() -> Dict[str, Any]:
}
``description`` is the skill's full SKILL.md frontmatter
``description:`` field. Note: the system prompt skill index
truncates this to the first 57 chars; see ``extract_skill_description``.
``description:`` field — the same string the system prompt renders
as `` - name: description`` for pre-existing skills.
"""
# Snapshot pre-reload state (name -> description) from the current
# slash-command cache. Using dicts lets the post-rescan diff carry
+6 -19
View File
@@ -779,31 +779,18 @@ def resolve_skill_config_values(
# ── Description extraction ────────────────────────────────────────────────
SKILL_PROMPT_DESC_LIMIT = 60
def _normalize_skill_description(frontmatter: Dict[str, Any]) -> str:
"""Normalize a skill's description field for comparison/truncation."""
raw_desc = frontmatter.get("description", "")
return str(raw_desc).strip().strip("'\"") if raw_desc else ""
def extract_skill_description(frontmatter: Dict[str, Any]) -> str:
"""Extract a system-prompt-length description from parsed frontmatter."""
desc = _normalize_skill_description(frontmatter)
if not desc:
"""Extract a truncated description from parsed frontmatter."""
raw_desc = frontmatter.get("description", "")
if not raw_desc:
return ""
if len(desc) > SKILL_PROMPT_DESC_LIMIT:
return desc[:SKILL_PROMPT_DESC_LIMIT - 3] + "..."
desc = str(raw_desc).strip().strip("'\"")
if len(desc) > 60:
return desc[:57] + "..."
return desc
def is_skill_description_truncated_for_prompt(frontmatter: Dict[str, Any]) -> bool:
"""True when the description will be truncated in the system prompt skill index."""
desc = _normalize_skill_description(frontmatter)
return len(desc) > SKILL_PROMPT_DESC_LIMIT
# ── File iteration ────────────────────────────────────────────────────────
-28
View File
@@ -525,34 +525,6 @@ def finalize_turn(
except Exception as exc:
logger.warning("post_llm_call hook failed: %s", exc)
# Context engine observation hook: notify the active engine that this
# turn has finished, with the finalized transcript. Complements the
# per-request select_context() hook (selection before the request;
# observation after the turn). No-op default, fail-open.
try:
from agent.conversation_loop import _notify_context_engine_turn_complete
# Forward the turn's canonical usage when the host has it. The loop
# stashes the most recent API response's usage dict (the same
# canonical buckets fed to ``update_from_response``) on the agent as
# ``_last_turn_usage``. It is ``None`` on turns that never reached a
# provider response (early failure / interrupt), which is exactly the
# contract: real usage when available, ``None`` otherwise.
_turn_usage = getattr(agent, "_last_turn_usage", None)
_notify_context_engine_turn_complete(
agent,
messages,
usage=_turn_usage,
logger=logger,
turn_id=turn_id,
task_id=effective_task_id,
api_call_count=api_call_count,
interrupted=interrupted,
failed=failed,
turn_exit_reason=_turn_exit_reason,
)
except Exception as exc:
logger.warning("on_turn_complete notification failed: %s", exc)
# Extract reasoning from the CURRENT turn only. Walk backwards
# but stop at the user message that started this turn — anything
# earlier is from a prior turn and must not leak into the reasoning
@@ -122,16 +122,11 @@ auxiliary:
// A normal message crosses the tiny configured context budget. The mock
// blocks only the resulting summary request, so these assertions run
// during automatic compaction rather than a slash-command path.
// The payload must cross threshold_tokens (22k) on its OWN weight
// (~12k tokens) on top of the system prompt. Do not shrink it: at
// repeat(500) the trigger only worked because the ambient system prompt
// (skills index + tool schemas) happened to carry it over the line, and
// a 160-token skills-index cleanup on main broke the test for a day.
await pasteAndSend(page, 'E2E_COMPACTION_HISTORY_ONE '.repeat(5))
await waitForTranscript(page, MOCK_REPLY)
await pasteAndSend(page, 'E2E_COMPACTION_HISTORY_TWO '.repeat(5))
await waitForTranscript(page, MOCK_REPLY)
await pasteAndSend(page, 'E2E_TRIGGER_AUTOMATIC_COMPACTION '.repeat(1500))
await pasteAndSend(page, 'E2E_TRIGGER_AUTOMATIC_COMPACTION '.repeat(500))
await fixture.mock.waitForHeldCompletion()
await expect(page.getByRole('status', { name: 'Summarizing thread' }).last()).toBeVisible()
@@ -1,54 +0,0 @@
import assert from 'node:assert/strict'
import { test } from 'vitest'
import { findGitBash } from './find-git-bash'
const yes = () => true
const no = () => false
test('HERMES_GIT_BASH_PATH override takes precedence', () => {
const result = findGitBash({
isWindows: true,
env: { HERMES_GIT_BASH_PATH: 'D:\\CustomGit\\bin\\bash.exe' },
fileExists: yes,
findOnPath: () => null
})
assert.equal(result, 'D:\\CustomGit\\bin\\bash.exe')
})
test('HERMES_GIT_BASH_PATH invalid path falls through to candidates', () => {
const env = {
HERMES_GIT_BASH_PATH: 'X:\\Missing\\bash.exe',
LOCALAPPDATA: 'C:\\Users\\test\\AppData\\Local',
ProgramFiles: 'C:\\Program Files',
'ProgramFiles(x86)': 'C:\\Program Files (x86)'
}
const fileExists = (p: string) => p !== 'X:\\Missing\\bash.exe' && p.includes('Program Files\\Git\\bin\\bash.exe')
const result = findGitBash({ isWindows: true, env, fileExists, findOnPath: () => null })
assert.equal(result, 'C:\\Program Files\\Git\\bin\\bash.exe')
})
test('HERMES_GIT_BASH_PATH empty string is ignored', () => {
const result = findGitBash({
isWindows: true,
env: { HERMES_GIT_BASH_PATH: '', LOCALAPPDATA: '' },
fileExists: no,
findOnPath: () => 'C:\\msys64\\usr\\bin\\bash.exe'
})
assert.equal(result, 'C:\\msys64\\usr\\bin\\bash.exe')
})
test('non-Windows uses findOnPath', () => {
const result = findGitBash({
isWindows: false,
env: {},
fileExists: no,
findOnPath: () => '/usr/bin/bash'
})
assert.equal(result, '/usr/bin/bash')
})
-67
View File
@@ -1,67 +0,0 @@
import path from 'node:path'
export interface GitBashOptions {
isWindows: boolean
env: Record<string, string | undefined>
fileExists: (filePath: string) => boolean
findOnPath?: (command: string) => string | null
}
/**
* Locate bash.exe on Windows.
* Resolution order (first match wins):
* 1. HERMES_GIT_BASH_PATH env var override
* 2. PortableGit under %LOCALAPPDATA%\hermes\git\ (install.ps1)
* 3. Standard Git for Windows install locations
* 4. %LOCALAPPDATA%\Programs\Git\ (user-scoped)
* 5. bash on PATH
*/
export function findGitBash(opts: GitBashOptions): string | null {
const { isWindows, env, fileExists, findOnPath } = opts
if (!isWindows) {
return findOnPath ? findOnPath('bash') : null
}
// Respect HERMES_GIT_BASH_PATH if set (mirrors tools/environments/local.py:_find_bash).
const gitBashPath = env.HERMES_GIT_BASH_PATH
if (gitBashPath && fileExists(gitBashPath)) {
return gitBashPath
}
const localAppData = env.LOCALAPPDATA || ''
const candidates: string[] = []
// Candidate paths are Windows paths regardless of host platform (tests run
// on POSIX CI hosts too), so join with win32 semantics explicitly.
const joinWin = path.win32.join
if (localAppData) {
candidates.push(joinWin(localAppData, 'hermes', 'git', 'bin', 'bash.exe'))
candidates.push(joinWin(localAppData, 'hermes', 'git', 'usr', 'bin', 'bash.exe'))
}
candidates.push(joinWin(env['ProgramFiles'] || 'C:\\Program Files', 'Git', 'bin', 'bash.exe'))
candidates.push(joinWin(env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Git', 'bin', 'bash.exe'))
if (localAppData) {
candidates.push(joinWin(localAppData, 'Programs', 'Git', 'bin', 'bash.exe'))
}
for (const candidate of candidates) {
if (fileExists(candidate)) {
return candidate
}
}
if (findOnPath) {
const onPath = findOnPath('bash')
if (onPath) {
return onPath
}
}
return null
}
+41 -10
View File
@@ -78,7 +78,6 @@ import {
} from './desktop-uninstall'
import { installEmbedReferer } from './embed-referer'
import { createEventDeduper } from './event-dedupe'
import { findGitBash as _findGitBash } from './find-git-bash'
import { readDirForIpc } from './fs-read-dir'
import { probeGatewayWebSocket } from './gateway-ws-probe'
import { scanGitRepos } from './git-repo-scan'
@@ -1916,16 +1915,48 @@ function findSystemPython() {
return null
}
// findGitBash — locate bash.exe on Windows. Resolves HERMES_GIT_BASH_PATH
// first (mirrors tools/environments/local.py:_find_bash), then PortableGit,
// standard install locations, and finally PATH.
// findGitBash — locate bash.exe on Windows. Hermes' terminal tool requires
// bash (POSIX shell), and on Windows that's almost always Git for Windows'
// bundled Git Bash. We check the same set of locations tools/environments/
// local.py:_find_bash() checks at runtime, so a positive result here means
// the agent will be able to start a terminal too.
//
// On non-Windows hosts bash is part of the OS and this just returns the
// first bash on PATH.
function findGitBash() {
return _findGitBash({
isWindows: IS_WINDOWS,
env: process.env,
fileExists,
findOnPath
})
if (!IS_WINDOWS) {
return findOnPath('bash')
}
// install.ps1 drops PortableGit at %LOCALAPPDATA%\hermes\git\... — checked
// first so users who installed via install.ps1 are detected before we
// start probing system-wide locations.
const localAppData = process.env.LOCALAPPDATA || ''
const candidates = []
if (localAppData) {
candidates.push(path.join(localAppData, 'hermes', 'git', 'bin', 'bash.exe'))
candidates.push(path.join(localAppData, 'hermes', 'git', 'usr', 'bin', 'bash.exe'))
}
// Standard Git for Windows install locations.
candidates.push(path.join(process.env['ProgramFiles'] || 'C:\\Program Files', 'Git', 'bin', 'bash.exe'))
candidates.push(path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Git', 'bin', 'bash.exe'))
if (localAppData) {
candidates.push(path.join(localAppData, 'Programs', 'Git', 'bin', 'bash.exe'))
}
for (const candidate of candidates) {
if (fileExists(candidate)) {
return candidate
}
}
// Last resort — bash on PATH (covers WSL bash, MSYS2, custom installs).
// On WSL hosts findOnPath itself filters out Windows-binary paths via
// isWindowsBinaryPathInWsl, so we won't hand back a wsl.exe shim either.
return findOnPath('bash')
}
function getVenvPython(venvRoot) {
@@ -112,8 +112,6 @@ const COMPACTION_RESUME_EVENT_TYPES = new Set([
'reasoning.available',
'moa.reference',
'moa.aggregating',
'moa.progress',
'moa.phase',
'tool.start',
'tool.progress',
'tool.generating',
@@ -526,26 +524,7 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
const cnt = typeof payload?.count === 'number' ? payload.count : undefined
const header = idx && cnt ? `◇ Reference ${idx}/${cnt} — ${label}` : `◇ Reference — ${label}`
const body = coerceThinkingText(payload?.text)
const text = `${header}\n${body}\n\n`
if (idx === undefined || idx <= 1) {
// First reference: clear any stale reasoning left over from
// before this turn's references start, same as before.
appendReasoningDelta(sessionId, text, true)
} else {
// Later references must accumulate, not replace — otherwise
// each new reference wipes out the ones already shown (#64658).
// Queue-then-flush (rather than the streamed/batched queue path)
// applies it immediately, since each reference arrives as one
// complete block rather than incremental tokens. reasoning.delta
// cannot be mid-flight here: MoAChatCompletions.reference_callback
// (agent/moa_loop.py) fires "moa.reference" once per reference's
// already-complete text, with no concurrent token stream for the
// reference-gathering phase, so there is no in-flight delta to
// collide with in the shared queue bucket.
appendReasoningDelta(sessionId, text, false)
flushQueuedDeltas(sessionId)
}
appendReasoningDelta(sessionId, `${header}\n${body}\n\n`, true)
}
if (isActiveEvent) {
@@ -554,38 +533,6 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
} else if (event.type === 'moa.aggregating') {
// Status transition only; the aggregator's reply arrives via the normal
// message stream. No reasoning/transcript mutation here.
if (isActiveEvent) {
setPetActivity({ reasoning: true })
}
} else if (event.type === 'moa.progress') {
// Live reference fan-out progress ("refs k/n") — surfaced in the same
// reasoning disclosure the references land in. These lines arrive
// BEFORE any moa.reference event (references are only emitted once the
// whole fan-out completes), and the first moa.reference replaces the
// block, so the progress trail is self-cleaning.
if (sessionId && typeof payload?.refs_done === 'number' && typeof payload?.refs_total === 'number') {
const label = coerceGatewayText(payload?.label)
const line = label
? `◇ MoA refs ${payload.refs_done}/${payload.refs_total} — ${label}\n`
: `◇ MoA refs ${payload.refs_done}/${payload.refs_total}\n`
appendReasoningDelta(sessionId, line, payload.refs_done <= 1)
flushQueuedDeltas(sessionId)
}
if (isActiveEvent) {
setPetActivity({ reasoning: true })
}
} else if (event.type === 'moa.phase') {
// Phase transition — currently only phase="aggregator" (fan-out done,
// aggregator acting). Append a one-line marker; the first
// moa.reference that follows replaces the whole block.
if (sessionId && payload?.phase === 'aggregator') {
appendReasoningDelta(sessionId, '◇ MoA aggregating…\n', false)
flushQueuedDeltas(sessionId)
}
if (isActiveEvent) {
setPetActivity({ reasoning: true })
}
@@ -1,123 +0,0 @@
import { QueryClient } from '@tanstack/react-query'
import { act, cleanup, render, waitFor } from '@testing-library/react'
import { useEffect, useRef } from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { ClientSessionState } from '@/app/types'
import { createClientSessionState } from '@/lib/chat-runtime'
import type { RpcEvent } from '@/types/hermes'
import { useMessageStream } from './index'
const SID = 'session-1'
let handleEvent: ((event: RpcEvent) => void) | null = null
let latestState: ClientSessionState | null = null
function Harness() {
const activeSessionIdRef = useRef<string | null>(SID)
const sessionStateByRuntimeIdRef = useRef(new Map<string, ClientSessionState>())
const queryClientRef = useRef(new QueryClient())
const stream = useMessageStream({
activeSessionIdRef,
hydrateFromStoredSession: vi.fn(async () => undefined),
queryClient: queryClientRef.current,
refreshHermesConfig: vi.fn(async () => undefined),
refreshSessions: vi.fn(async () => undefined),
sessionStateByRuntimeIdRef,
updateSessionState: (sessionId, updater) => {
const current = sessionStateByRuntimeIdRef.current.get(sessionId) ?? createClientSessionState()
const next = updater(current)
sessionStateByRuntimeIdRef.current.set(sessionId, next)
latestState = next
return next
}
})
useEffect(() => {
handleEvent = stream.handleGatewayEvent
}, [stream.handleGatewayEvent])
return null
}
async function mountStream() {
render(<Harness />)
await waitFor(() => expect(handleEvent).not.toBeNull())
}
function emit(type: RpcEvent['type'], payload: RpcEvent['payload'] = {}) {
act(() => handleEvent!({ payload, session_id: SID, type }))
}
function reasoningText(): string {
const message = latestState?.messages.at(-1)
const part = message?.parts.find(p => p.type === 'reasoning')
return part?.type === 'reasoning' ? part.text : ''
}
beforeEach(() => {
handleEvent = null
latestState = null
})
afterEach(() => {
cleanup()
vi.restoreAllMocks()
})
describe('useMessageStream moa.progress / moa.phase surfacing', () => {
it('shows refs k/n lines in the reasoning block as references complete', async () => {
await mountStream()
emit('message.start')
emit('moa.progress', { label: 'model-a', refs_done: 1, refs_total: 3 })
emit('moa.progress', { label: 'model-b', refs_done: 2, refs_total: 3 })
const text = reasoningText()
expect(text).toContain('MoA refs 1/3 — model-a')
expect(text).toContain('MoA refs 2/3 — model-b')
})
it('restarts the progress block on the first ref of a new fan-out', async () => {
await mountStream()
emit('message.start')
emit('moa.progress', { label: 'stale', refs_done: 1, refs_total: 2 })
emit('moa.progress', { label: 'stale-2', refs_done: 2, refs_total: 2 })
// A later turn's fan-out starts over at 1/N — old lines must not linger.
emit('moa.progress', { label: 'fresh', refs_done: 1, refs_total: 2 })
const text = reasoningText()
expect(text).toContain('MoA refs 1/2 — fresh')
expect(text).not.toContain('stale')
})
it('appends the aggregating marker on moa.phase and ignores unknown phases', async () => {
await mountStream()
emit('message.start')
emit('moa.progress', { label: 'model-a', refs_done: 1, refs_total: 1 })
emit('moa.phase', { phase: 'reference', refs_done: 1, refs_total: 1 })
expect(reasoningText()).not.toContain('aggregating')
emit('moa.phase', { aggregator: 'agg-model', phase: 'aggregator', refs_done: 1, refs_total: 1 })
expect(reasoningText()).toContain('MoA aggregating…')
})
it('a following moa.reference replaces the progress trail (self-cleaning)', async () => {
await mountStream()
emit('message.start')
emit('moa.progress', { label: 'model-a', refs_done: 1, refs_total: 1 })
emit('moa.phase', { phase: 'aggregator', refs_done: 1, refs_total: 1 })
emit('moa.reference', { count: 1, index: 1, label: 'model-a', text: 'advice-a' })
const text = reasoningText()
expect(text).toContain('Reference 1/1 — model-a')
expect(text).not.toContain('MoA refs')
expect(text).not.toContain('aggregating')
})
})
@@ -1,114 +0,0 @@
import { QueryClient } from '@tanstack/react-query'
import { act, cleanup, render, waitFor } from '@testing-library/react'
import { useEffect, useRef } from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { ClientSessionState } from '@/app/types'
import { createClientSessionState } from '@/lib/chat-runtime'
import type { RpcEvent } from '@/types/hermes'
import { useMessageStream } from './index'
const SID = 'session-1'
let handleEvent: ((event: RpcEvent) => void) | null = null
let latestState: ClientSessionState | null = null
function Harness() {
const activeSessionIdRef = useRef<string | null>(SID)
const sessionStateByRuntimeIdRef = useRef(new Map<string, ClientSessionState>())
const queryClientRef = useRef(new QueryClient())
const stream = useMessageStream({
activeSessionIdRef,
hydrateFromStoredSession: vi.fn(async () => undefined),
queryClient: queryClientRef.current,
refreshHermesConfig: vi.fn(async () => undefined),
refreshSessions: vi.fn(async () => undefined),
sessionStateByRuntimeIdRef,
updateSessionState: (sessionId, updater) => {
const current = sessionStateByRuntimeIdRef.current.get(sessionId) ?? createClientSessionState()
const next = updater(current)
sessionStateByRuntimeIdRef.current.set(sessionId, next)
latestState = next
return next
}
})
useEffect(() => {
handleEvent = stream.handleGatewayEvent
}, [stream.handleGatewayEvent])
return null
}
async function mountStream() {
render(<Harness />)
await waitFor(() => expect(handleEvent).not.toBeNull())
}
function emit(type: RpcEvent['type'], payload: RpcEvent['payload'] = {}) {
act(() => handleEvent!({ payload, session_id: SID, type }))
}
function reasoningText(): string {
const message = latestState?.messages.at(-1)
const part = message?.parts.find(p => p.type === 'reasoning')
return part?.type === 'reasoning' ? part.text : ''
}
describe('useMessageStream moa.reference accumulation (#64658)', () => {
beforeEach(() => {
handleEvent = null
latestState = null
})
afterEach(() => {
cleanup()
vi.restoreAllMocks()
})
it('keeps every reference model labelled block instead of only the latest one', async () => {
await mountStream()
emit('moa.reference', { count: 2, index: 1, label: 'model-a', text: 'advice-a' })
emit('moa.reference', { count: 2, index: 2, label: 'model-b', text: 'advice-b' })
const text = reasoningText()
expect(text).toContain('model-a')
expect(text).toContain('advice-a')
expect(text).toContain('model-b')
expect(text).toContain('advice-b')
})
it('handles a single-reference MoA turn (count=1) without regression', async () => {
await mountStream()
emit('moa.reference', { count: 1, index: 1, label: 'model-a', text: 'only-advice' })
const text = reasoningText()
expect(text).toContain('model-a')
expect(text).toContain('only-advice')
})
it('accumulates three or more references in order', async () => {
await mountStream()
emit('moa.reference', { count: 3, index: 1, label: 'model-a', text: 'advice-a' })
emit('moa.reference', { count: 3, index: 2, label: 'model-b', text: 'advice-b' })
emit('moa.reference', { count: 3, index: 3, label: 'model-c', text: 'advice-c' })
const text = reasoningText()
const orderOk =
text.indexOf('advice-a') < text.indexOf('advice-b') && text.indexOf('advice-b') < text.indexOf('advice-c')
expect(text).toContain('advice-a')
expect(text).toContain('advice-b')
expect(text).toContain('advice-c')
expect(orderOk).toBe(true)
})
})
@@ -435,51 +435,4 @@ describe('ModelSettings MoA preset editor', () => {
vi.useRealTimers()
}
})
it('autosaves the selected preset when its enabled switch is toggled', async () => {
vi.useFakeTimers({ shouldAdvanceTime: true })
try {
await openReferenceEditor()
fireEvent.click(screen.getByRole('switch', { name: 'Enabled' }))
await vi.advanceTimersByTimeAsync(700)
expect(saveMoaModels).toHaveBeenCalledWith(
expect.objectContaining({
presets: expect.objectContaining({
default: expect.objectContaining({ enabled: false })
})
})
)
} finally {
vi.useRealTimers()
}
})
it('saves a disabled reference model without removing it (per-slot enabled toggle)', async () => {
vi.useFakeTimers({ shouldAdvanceTime: true })
try {
await openReferenceEditor()
fireEvent.click(screen.getByRole('switch', { name: 'Disable reference 1' }))
await vi.advanceTimersByTimeAsync(700)
expect(saveMoaModels).toHaveBeenCalledWith(
expect.objectContaining({
presets: expect.objectContaining({
default: expect.objectContaining({
reference_models: [
expect.objectContaining({ provider: 'nous', model: 'hermes-4', enabled: false }),
expect.objectContaining({ provider: 'openrouter', model: 'deepseek/deepseek-v4-pro' })
]
})
})
})
)
} finally {
vi.useRealTimers()
}
})
})
@@ -993,15 +993,6 @@ export function ModelSettings({ onMainModelChanged }: ModelSettingsProps) {
))}
</SelectContent>
</Select>
<label className="flex items-center gap-2 rounded-sm border border-border px-2 py-1 text-xs">
Enabled
<Switch
checked={currentMoaPreset.enabled !== false}
disabled={applying}
onCheckedChange={checked => updateMoaPreset(prev => ({ ...prev, enabled: checked }))}
size="xs"
/>
</label>
<Button
disabled={applying}
onClick={() => {
@@ -1078,21 +1069,6 @@ export function ModelSettings({ onMainModelChanged }: ModelSettingsProps) {
<div className="grid gap-1">
{currentMoaPreset.reference_models.map((slot, index) => (
<ListRow
action={
<Switch
aria-label={`${slot.enabled !== false ? 'Disable' : 'Enable'} reference ${index + 1}`}
checked={slot.enabled !== false}
disabled={applying}
onCheckedChange={checked =>
updateMoaPreset(prev => ({
...prev,
reference_models: prev.reference_models.map((s, i) =>
i === index ? { ...s, enabled: checked === true } : s
)
}))
}
/>
}
below={
<div className="mt-2 flex flex-wrap items-center gap-2 pt-1">
<Select
@@ -1161,7 +1137,6 @@ export function ModelSettings({ onMainModelChanged }: ModelSettingsProps) {
</Button>
</div>
}
className={cn(slot.enabled === false && 'opacity-60')}
description={
<span className="font-mono text-[0.68rem]">
{slot.provider} · {slot.model || m.model}
@@ -1174,10 +1149,7 @@ export function ModelSettings({ onMainModelChanged }: ModelSettingsProps) {
<Button
disabled={applying}
onClick={() =>
updateMoaPreset(prev => ({
...prev,
reference_models: [...prev.reference_models, { ...prev.aggregator, enabled: true }]
}))
updateMoaPreset(prev => ({ ...prev, reference_models: [...prev.reference_models, prev.aggregator] }))
}
size="sm"
variant="textStrong"
+2 -4
View File
@@ -111,8 +111,7 @@ export function ListRow({
hint,
action,
below,
wide = false,
className
wide = false
}: {
title: ReactNode
description?: ReactNode
@@ -120,13 +119,12 @@ export function ListRow({
action?: ReactNode
below?: ReactNode
wide?: boolean
className?: string
}) {
return (
// Container-queried, not viewport-queried: the label/control split keys on
// the row's own pane width, so a narrow detail column (messaging, split
// views) stacks instead of squishing the label against minmax(15rem,…).
<div className={cn('@container', className)}>
<div className="@container">
<div
className={cn(
'grid gap-3 py-3',
-4
View File
@@ -94,10 +94,6 @@ export type GatewayEventPayload = {
label?: string
index?: number
aggregator?: string
// moa.progress / moa.phase (Mixture of Agents fan-out progress relay)
refs_done?: number
refs_total?: number
phase?: string
// message.complete — signals the final text was already previewed via
// interim_assistant_callback, so the UI can settle instead of duplicating.
response_previewed?: boolean
+1 -6
View File
@@ -999,7 +999,6 @@ export interface MoaModelSlot {
model: string
/** Optional per-slot reasoning effort — round-tripped, not edited here. */
reasoning_effort?: string
enabled?: boolean
}
export interface MoaConfigResponse {
@@ -1010,26 +1009,22 @@ export interface MoaConfigResponse {
{
aggregator: MoaModelSlot
aggregator_temperature: number
degraded_reference_policy: 'loud' | 'silent'
enabled: boolean
max_tokens: number
reference_models: MoaModelSlot[]
reference_temperature: number
/** Optional advisor output cap — round-tripped, not edited here. */
reference_max_tokens?: number | null
/** Fan-out cadence (user_turn default | per_iteration | every_n:N) — round-tripped. */
/** Fan-out cadence (per_iteration | user_turn) — round-tripped. */
fanout?: string
reference_timeout: number | null
}
>
aggregator: MoaModelSlot
aggregator_temperature: number
degraded_reference_policy: 'loud' | 'silent'
enabled: boolean
max_tokens: number
reference_models: MoaModelSlot[]
reference_temperature: number
reference_timeout: number | null
}
export interface ModelAssignmentRequest {
-12
View File
@@ -407,18 +407,6 @@ compression:
# Enable automatic context compression (default: true)
# Set to false if you prefer to manage context manually or want errors on overflow
enabled: true
# Opt-in compression progress notices on chat platforms (default: false).
# By design, routine automatic compression is SILENT on human-facing chat
# gateways (Telegram, Discord, Slack, ...) — it happens in the background
# with server-side logging only. Set true to also deliver the routine
# progress statuses (compacting started, preflight/pre-API compression,
# idle compaction, retry progress, and the compaction-complete notice) to
# chat platforms. Unrelated operational noise (auxiliary model failures,
# provider retry chatter) stays suppressed either way, and compression
# FAILURE notices + manual /compress feedback are always visible
# regardless of this setting. (#52995)
progress_notices: false
# Trigger compression at this % of model's context limit (default: 0.50 = 50%)
# Lower values = more aggressive compression, higher values = compress later
@@ -1 +0,0 @@
akattelu
-1
View File
@@ -1 +0,0 @@
AlanBurningsuit
@@ -1 +0,0 @@
chaos-xxl
@@ -1 +0,0 @@
hellofrommorgan
@@ -1 +0,0 @@
quantumbyte1617
@@ -1 +0,0 @@
immuhammadfurqan
-1
View File
@@ -1 +0,0 @@
VerbalChainsaw
-1
View File
@@ -1 +0,0 @@
robbyczgw-cla
@@ -1 +0,0 @@
oppenheimor
@@ -1 +0,0 @@
upicat
-2
View File
@@ -1,2 +0,0 @@
seamusmore
# PR #64339 salvage
+2 -21
View File
@@ -1332,29 +1332,10 @@ class WebhookAdapter(BasePlatformAdapter):
error=f"No chat_id or home channel for {platform_name}",
)
# Pass platform-specific thread routing/creation hints through the
# common adapter metadata channel. An explicit thread_id wins over
# create_thread so all later sends from one webhook run stay together.
# Pass thread_id from deliver_extra so Telegram forum topics work
metadata = None
thread_id = extra.get("message_thread_id") or extra.get("thread_id")
if thread_id:
metadata = {"thread_id": thread_id}
if platform_name == "discord" and extra.get("create_thread") and not thread_id:
metadata = metadata or {}
metadata["create_thread"] = True
if extra.get("thread_name"):
metadata["thread_name"] = extra["thread_name"]
result = await adapter.send(chat_id, content, metadata=metadata)
# Agent-mode webhooks may emit interim messages before their final
# answer. Once Discord creates the event thread, pin subsequent sends
# for this delivery to that same thread instead of creating siblings.
if (
platform_name == "discord"
and result.success
and isinstance(result.raw_response, dict)
and result.raw_response.get("thread_id")
):
extra["thread_id"] = str(result.raw_response["thread_id"])
return result
return await adapter.send(chat_id, content, metadata=metadata)
+9 -145
View File
@@ -54,16 +54,6 @@ from typing import Awaitable, Callable, Dict, Optional, Any, List, Union, cast
# preserving the established test-patch surface.
from agent.account_usage import fetch_account_usage, render_account_usage_lines
from agent.async_utils import consume_detached_task_result, safe_schedule_threadsafe
from agent.conversation_compression import (
COMPACTION_STATUS,
COMPRESSION_RETRY_CONTEXT_REDUCED_STATUS_TEMPLATE,
COMPRESSION_RETRY_MESSAGES_STATUS_TEMPLATE,
COMPRESSION_RETRY_TOKENS_STATUS_TEMPLATE,
COMPRESSION_RETRY_TOO_LARGE_STATUS_TEMPLATE,
IDLE_COMPACTION_STATUS_TEMPLATE,
PRE_API_COMPRESSION_STATUS_TEMPLATE,
PREFLIGHT_COMPRESSION_STATUS_TEMPLATE,
)
from agent.conversation_loop import INTERRUPT_WAITING_FOR_MODEL_PREFIX
from agent.i18n import t
from hermes_cli.config import cfg_get
@@ -116,70 +106,6 @@ _TELEGRAM_NOISY_STATUS_RE = re.compile(
re.IGNORECASE | re.DOTALL,
)
def _status_template_to_regex(template: str) -> str:
"""Compile a compression status template constant into a regex source.
Literal text is escaped verbatim (so wording drift in
agent/conversation_compression.py cannot silently diverge from this
matcher — the constants ARE the wording) and each ``{field}`` format
placeholder is replaced with a numeric-ish pattern covering every value
the emit sites format in (ints, ``{:,}`` thousands separators).
"""
parts = re.split(r"\{[^{}]*\}", template)
return r"[\d,]+".join(re.escape(part) for part in parts)
# ROUTINE compression progress statuses, derived from the SAME template
# constants the emit sites format (agent/conversation_compression.py, #69550)
# — never re-inlined wording. Used ONLY by the opt-in
# ``compression.progress_notices`` gate below (#52995) to decide which of the
# noisy statuses matched by _TELEGRAM_NOISY_STATUS_RE are compression
# progress (deliverable when the user opted in) versus unrelated aux/retry
# chatter (always suppressed on chat surfaces). Failure notices and manual
# /compress feedback never match _TELEGRAM_NOISY_STATUS_RE in the first
# place, so they are unaffected by this gate.
_COMPRESSION_PROGRESS_STATUS_RE = re.compile(
"|".join(
_status_template_to_regex(_template)
for _template in (
COMPACTION_STATUS,
PRE_API_COMPRESSION_STATUS_TEMPLATE,
PREFLIGHT_COMPRESSION_STATUS_TEMPLATE,
IDLE_COMPACTION_STATUS_TEMPLATE,
COMPRESSION_RETRY_TOO_LARGE_STATUS_TEMPLATE,
COMPRESSION_RETRY_MESSAGES_STATUS_TEMPLATE,
COMPRESSION_RETRY_TOKENS_STATUS_TEMPLATE,
COMPRESSION_RETRY_CONTEXT_REDUCED_STATUS_TEMPLATE,
)
),
re.IGNORECASE,
)
def _gateway_compression_progress_notices_enabled() -> bool:
"""True when the user opted into routine compression progress notices.
Reads ``compression.progress_notices`` from the gateway's raw YAML config
(#52995). Default False — routine compression stays silent-by-design on
chat platforms unless explicitly enabled. Read live (mtime-cached) so a
config edit on a running gateway takes effect on the next status.
Fail-closed: any config read error keeps the silent default.
"""
try:
config = _load_gateway_config()
compression_cfg = config.get("compression") if isinstance(config, dict) else None
if isinstance(compression_cfg, dict):
return str(compression_cfg.get("progress_notices", False)).strip().lower() in {
"true",
"1",
"yes",
"on",
}
except Exception:
pass
return False
# Surfaces that consume gateway text programmatically (CLI/TUI "local"
# diagnostics, API JSON, webhook payloads) and therefore must keep RAW
# status/error text. EVERY other platform is a human-facing chat surface
@@ -569,17 +495,7 @@ def _prepare_gateway_status_message(platform: Any, event_type: str, message: str
text = _redact_gateway_user_facing_secrets(text)
if _TELEGRAM_NOISY_STATUS_RE.search(text):
# Opt-in #52995: `compression.progress_notices: true` lets ROUTINE
# compression progress statuses through to chat platforms. The
# membership check is derived from the #69550 template constants, so
# non-compression noise (aux failures, provider retry chatter, ...)
# stays suppressed even when the gate is open. Default False keeps
# the silent-by-design behavior byte-identical.
if not (
_gateway_compression_progress_notices_enabled()
and _COMPRESSION_PROGRESS_STATUS_RE.search(text)
):
return None
return None
if _looks_like_gateway_provider_error(text):
return _gateway_provider_error_reply(text)
return text
@@ -7054,10 +6970,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
# that triggered the /restart command closing its console.
if sys.platform == "win32":
import textwrap
from hermes_cli._subprocess_compat import (
windows_detach_flags_without_breakaway,
windows_detach_popen_kwargs,
)
from hermes_cli._subprocess_compat import windows_detach_popen_kwargs
cmd_argv = [*hermes_cmd, "gateway", "restart"]
watcher = textwrap.dedent(
@@ -7129,61 +7042,13 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
if watcher_env.get("PYTHONPATH"):
pythonpath.append(watcher_env["PYTHONPATH"])
watcher_env["PYTHONPATH"] = os.pathsep.join(dict.fromkeys(pythonpath))
watcher_argv = [
watcher_python,
"-c",
watcher,
str(current_pid),
str(restart_after_s),
*cmd_argv,
]
# The watcher process must itself break away from any job object the
# parent CLI lives in (Electron/Tauri-wrapped Hermes Desktop, Windows
# Terminal, schtasks shells); otherwise it is reaped when the CLI
# exits and the gateway never respawns. windows_detach_popen_kwargs()
# carries CREATE_BREAKAWAY_FROM_JOB, but a restrictive job object
# (no JOB_OBJECT_LIMIT_BREAKAWAY_OK) rejects that bit with
# ERROR_ACCESS_DENIED, surfaced as OSError. Retry once without the
# breakaway bit, preserving argv and the scrubbed watcher_env.
# Mirrors the canonical fallback in
# hermes_cli/gateway_windows.py::_spawn_detached.
try:
subprocess.Popen(
watcher_argv,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
env=watcher_env,
**windows_detach_popen_kwargs(),
)
except OSError:
try:
subprocess.Popen(
watcher_argv,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
env=watcher_env,
creationflags=windows_detach_flags_without_breakaway(),
)
except OSError as exc:
# Both spawn attempts failed (a breakaway-denying job object
# is the common cause, but OSError covers others too).
# Record a minimal, path-safe diagnostic and return without
# crashing the caller: state plainly that no watcher was
# started, and log only the interpreter basename and a
# numeric error code — never argv, env, watcher source, or
# str(exc) (which can carry a full interpreter path for a
# FileNotFoundError).
winerror = getattr(exc, "winerror", None)
error_code = winerror if winerror is not None else exc.errno
error_field = "winerror" if winerror is not None else "errno"
logger.warning(
"Detached restart watcher was not started after the "
"no-breakaway retry (%s; %s=%r). The gateway will not "
"be respawned by this restart attempt.",
os.path.basename(watcher_python),
error_field,
error_code,
)
subprocess.Popen(
[watcher_python, "-c", watcher, str(current_pid), str(restart_after_s), *cmd_argv],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
env=watcher_env,
**windows_detach_popen_kwargs(),
)
return
cmd = " ".join(shlex.quote(part) for part in hermes_cmd)
@@ -18250,7 +18115,6 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
("model", "context_length"),
("model", "max_tokens"),
("compression", "enabled"),
("compression", "progress_notices"),
("compression", "threshold"),
("compression", "model_thresholds"),
("compression", "threshold_tokens"),
-38
View File
@@ -36,7 +36,6 @@ from typing import Sequence
__all__ = [
"IS_WINDOWS",
"resolve_node_command",
"suppress_platform_ver_console",
"windows_detach_flags",
"windows_detach_flags_without_breakaway",
"windows_hide_flags",
@@ -227,43 +226,6 @@ def windows_hide_flags() -> int:
return _CREATE_NO_WINDOW
def suppress_platform_ver_console() -> None:
"""Stub out ``platform._syscmd_ver`` on Windows so it can never flash a
console window. No-op on non-Windows.
CPython's ``platform.win32_ver()`` — reached by ``platform.uname()``,
``platform.version()``, and ``platform.platform()`` — unconditionally
shells out ``cmd /c ver`` via ``subprocess.check_output(..., shell=True)``
with no ``CREATE_NO_WINDOW``. From a windowless parent (the pythonw
gateway and every kanban worker it spawns) that allocates a fresh
*visible* console: one flashing ``cmd`` window per process, triggered by
any dependency that merely touches ``platform.uname()`` at import time.
With ``_syscmd_ver`` stubbed to return its inputs, ``win32_ver()`` hits
the documented ``ValueError`` fallback and reads the version from
``sys.getwindowsversion().platform_version`` — same information, queried
in-process, no subprocess, no window. Verified equivalent on
CPython 3.11 (``platform()`` → ``Windows-10-10.0.xxxxx-SP0`` either way).
Call early, before heavyweight imports — the flash typically happens
during a dependency's import, not from Hermes' own code.
"""
if not IS_WINDOWS:
return
try:
import platform
if hasattr(platform, "_syscmd_ver"):
def _quiet_syscmd_ver(system="", release="", version="",
supported_platforms=("win32", "win16", "dos")):
return system, release, version
platform._syscmd_ver = _quiet_syscmd_ver
except Exception:
# Purely cosmetic hardening — never let it break startup.
pass
def windows_detach_popen_kwargs() -> dict:
"""Return a dict of Popen kwargs that detach a child on Windows and
fall back to the POSIX equivalent (``start_new_session=True``) on
+1 -2
View File
@@ -967,8 +967,7 @@ def discord_skill_commands_by_category(
Skills whose directory is nested at least 2 levels under a scan root
(e.g. ``creative/ascii-art/SKILL.md``) are grouped by their top-level
category. Root-level skills (e.g. ``some-skill/SKILL.md`` directly under a
scan root) are returned as
category. Root-level skills (e.g. ``dogfood/SKILL.md``) are returned as
*uncategorized*.
Scan roots include the local ``SKILLS_DIR`` **and** any configured
-19
View File
@@ -1398,15 +1398,6 @@ DEFAULT_CONFIG = {
"compression": {
"enabled": True,
"progress_notices": False, # opt-in (#52995): when True, routine compression
# progress statuses (compacting/preflight/pre-API/
# idle/retry) are delivered to chat gateway
# platforms instead of being suppressed by the
# gateway noise filter. Default False keeps
# routine compression silent-by-design on chat
# surfaces (server-side logging only). Failure
# notices and manual /compress feedback are
# always visible regardless of this setting.
"threshold": 0.50, # compress when context usage exceeds this ratio.
# Models with context windows below 512K are
# floored at 0.75 (raise-only) so compaction
@@ -2440,16 +2431,6 @@ DEFAULT_CONFIG = {
# override the output directory.
"save_traces": False,
"trace_dir": "",
# Privacy redaction filter for advisor (reference) outputs. Advisors
# can echo PII from the conversation (emails, formatted phone numbers)
# and credential shapes into reference blocks, traces, and the
# aggregator prompt. Modes ('' = off, the default):
# "display" — redact user-visible surfaces only (reference blocks
# shown in the UI + saved MoA trace records); the
# aggregator still sees raw advisor text.
# "full" — additionally redact the advisor text injected into
# the aggregator prompt (issue #59959).
"privacy_filter": "",
"presets": {
"default": {
"reference_models": [
-9
View File
@@ -61,15 +61,6 @@ try:
except ModuleNotFoundError:
pass
# Windows: neutralize CPython's ``platform._syscmd_ver`` before anything else
# imports — it shells out ``cmd /c ver`` (shell=True, no CREATE_NO_WINDOW), so
# any dependency touching ``platform.uname()`` at import time flashes a
# visible console when this process is windowless (pythonw gateway + every
# kanban worker). No-op on POSIX; never raises.
from hermes_cli._subprocess_compat import suppress_platform_ver_console
suppress_platform_ver_console()
import os
import sys
+1 -3
View File
@@ -111,9 +111,7 @@ def cmd_moa(args) -> None:
idx = 0
while True:
base = existing[idx] if idx < len(existing) else None
picked = _pick_slot(base)
picked["enabled"] = bool((base or {}).get("enabled", True))
refs.append(picked)
refs.append(_pick_slot(base))
idx += 1
choice = _prompt_choice("Add another reference model?", ["Add another", "Done"], 1)
if choice == 1:
+15 -139
View File
@@ -4,7 +4,6 @@ from __future__ import annotations
import base64
import json
import math
from copy import deepcopy
from typing import Any
@@ -21,12 +20,6 @@ DEFAULT_MOA_AGGREGATOR: dict[str, str] = {
"model": "anthropic/claude-opus-4.8",
}
DEFAULT_MOA_REFERENCE_TIMEOUT: float | None = None
def _default_reference_models() -> list[dict[str, Any]]:
return [{**slot, "enabled": True} for slot in deepcopy(DEFAULT_MOA_REFERENCE_MODELS)]
def _coerce_float_or_none(value: Any) -> float | None:
"""Coerce to a float, or None when unset/blank/invalid.
@@ -44,33 +37,6 @@ def _coerce_float_or_none(value: Any) -> float | None:
return None
def _coerce_reference_timeout(value: Any) -> float | None:
"""Return a finite positive advisor timeout, or None to inherit.
``None`` (the default) means "no per-preset override": the reference
fan-out inherits the ``auxiliary.moa_reference.timeout`` config value
(900s by default) via ``call_llm``'s own resolution, exactly like every
other auxiliary task. An explicit finite positive per-preset value is
honored as-is — no artificial cap, since long-thinking advisor models
legitimately run far beyond five minutes.
"""
if value is None or value == "" or isinstance(value, bool):
return DEFAULT_MOA_REFERENCE_TIMEOUT
try:
timeout = float(value)
except (TypeError, ValueError):
return DEFAULT_MOA_REFERENCE_TIMEOUT
if not math.isfinite(timeout) or timeout <= 0:
return DEFAULT_MOA_REFERENCE_TIMEOUT
return timeout
def _coerce_degraded_reference_policy(value: Any) -> str:
"""Normalize failed-advisor disclosure policy; unknown values fail loud."""
policy = str(value or "loud").strip().lower()
return policy if policy in {"loud", "silent"} else "loud"
def _coerce_int(value: Any, default: int) -> int:
if value is None or value == "":
return default
@@ -102,64 +68,9 @@ def _coerce_int_or_none(value: Any) -> int | None:
def _coerce_fanout(value: Any) -> str:
"""Normalize the fan-out cadence; unknown values fall back to default.
Canonical values are the strings ``per_iteration``, ``user_turn``, and
``every_n:<N>`` (N >= 2). The ``every_n`` cadence also accepts the mapping
form ``{mode: every_n, n: N}`` from hand-edited YAML and normalizes it to
the canonical string, so the rest of the pipeline (presets, flattened
view, runtime) only ever sees one shape. ``every_n:1`` semantically means
"run every iteration" and collapses to ``per_iteration``; anything
unparseable falls back to ``user_turn`` (the default — cheapest cadence;
see #67199).
"""
if isinstance(value, dict):
# Mapping form: {mode: every_n, n: 3}. Non-every_n mapping modes fall
# through to the string path below (e.g. {mode: user_turn}).
mode = str(value.get("mode") or "").strip().lower()
if mode == "every_n":
n = _coerce_int(value.get("n"), 0)
if n >= 2:
return f"every_n:{n}"
return "per_iteration" if n == 1 else "user_turn"
value = mode
"""Normalize the fan-out cadence; unknown values fall back to default."""
mode = str(value or "").strip().lower()
if mode in {"per_iteration", "user_turn"}:
return mode
if mode.startswith("every_n"):
_, sep, rest = mode.partition(":")
n = _coerce_int(rest.strip(), 0) if sep else 0
if n >= 2:
return f"every_n:{n}"
if n == 1:
return "per_iteration"
return "user_turn"
def coerce_privacy_filter(value: Any) -> str:
"""Normalize ``moa.privacy_filter`` to '' (off), 'display', or 'full'.
- ``''`` (empty string): filter off — the default. ``false``/``None``/
unknown values land here so a hand-edited config degrades to prior
behavior (tolerant-read contract).
- ``'display'``: redact user-visible surfaces only — the reference blocks
shown in the UI and the saved MoA trace records. The aggregator still
sees raw advisor text, so answer quality is unaffected.
- ``'full'``: additionally redact the advisor text injected into the
aggregator prompt (issue #59959's literal ask). A hand-edited boolean
``true`` maps here because the issue framed the toggle as "redact
before passing to the aggregator".
"""
if value is True:
return "full"
if value is None or value is False:
return ""
mode = str(value).strip().lower()
if mode in {"display", "full"}:
return mode
if mode in {"true", "on", "yes", "1"}:
return "full"
return ""
return mode if mode in {"per_iteration", "user_turn"} else "per_iteration"
def _clean_reasoning_effort(value: Any) -> str | None:
@@ -176,22 +87,7 @@ def _clean_reasoning_effort(value: Any) -> str | None:
return parsed.get("effort")
def _coerce_bool(value: Any, default: bool = True) -> bool:
if value is None:
return default
if isinstance(value, bool):
return value
if isinstance(value, str):
text = value.strip().lower()
if text in {"0", "false", "no", "off"}:
return False
if text in {"1", "true", "yes", "on"}:
return True
return default
return bool(value)
def _clean_slot(slot: Any, *, include_enabled: bool = False) -> dict[str, Any] | None:
def _clean_slot(slot: Any) -> dict[str, Any] | None:
if not isinstance(slot, dict):
return None
provider = str(slot.get("provider") or "").strip()
@@ -217,8 +113,6 @@ def _clean_slot(slot: Any, *, include_enabled: bool = False) -> dict[str, Any] |
slot_mt = _coerce_int_or_none(slot.get("max_tokens"))
if slot_mt is not None:
clean["max_tokens"] = slot_mt
if include_enabled:
clean["enabled"] = _coerce_bool(slot.get("enabled"), True)
return clean
@@ -295,17 +189,15 @@ def validate_moa_payload(raw: Any) -> list[str]:
def _default_preset() -> dict[str, Any]:
return {
"reference_models": _default_reference_models(),
"reference_models": deepcopy(DEFAULT_MOA_REFERENCE_MODELS),
"aggregator": deepcopy(DEFAULT_MOA_AGGREGATOR),
# None = temperature omitted from API calls (provider default),
# matching single-model agent behavior.
"reference_temperature": None,
"aggregator_temperature": None,
"reference_timeout": DEFAULT_MOA_REFERENCE_TIMEOUT,
"degraded_reference_policy": "loud",
"max_tokens": 4096,
"reference_max_tokens": None,
"fanout": "user_turn",
"fanout": "per_iteration",
"enabled": True,
}
@@ -326,23 +218,19 @@ def _normalize_preset(raw: Any) -> dict[str, Any]:
# defaults instead of crashing the iteration, mirroring the tolerance
# for the scalar fields below (reference_temperature / max_tokens).
raw_refs = [raw_refs] if isinstance(raw_refs, dict) else []
refs = [_clean_slot(item, include_enabled=True) for item in raw_refs]
refs = [_clean_slot(item) for item in raw_refs]
refs = [item for item in refs if item is not None]
if not refs:
refs = _default_reference_models()
refs = deepcopy(DEFAULT_MOA_REFERENCE_MODELS)
aggregator = _clean_slot(raw.get("aggregator")) or deepcopy(DEFAULT_MOA_AGGREGATOR)
return {
"enabled": _coerce_bool(raw.get("enabled"), True),
"enabled": bool(raw.get("enabled", True)),
"reference_models": refs,
"aggregator": aggregator,
"reference_temperature": _coerce_float_or_none(raw.get("reference_temperature")),
"aggregator_temperature": _coerce_float_or_none(raw.get("aggregator_temperature")),
"reference_timeout": _coerce_reference_timeout(raw.get("reference_timeout")),
"degraded_reference_policy": _coerce_degraded_reference_policy(
raw.get("degraded_reference_policy")
),
"max_tokens": _coerce_int(raw.get("max_tokens"), 4096),
# Optional cap on how much each reference ADVISOR may generate per turn.
# None (default) = uncapped: advisors write full-length advice, matching
@@ -353,18 +241,12 @@ def _normalize_preset(raw: Any) -> dict[str, Any]:
# judgement, so capping roughly halves per-turn wall time. Does NOT cap
# the acting aggregator (its output is the user-visible answer).
"reference_max_tokens": _coerce_int_or_none(raw.get("reference_max_tokens")),
# When the reference fan-out runs. "user_turn" (default) runs the
# advisors ONCE per user turn (the original MoA shape, and the
# cheapest cadence — #67199): the aggregator gets their upfront
# plan-level advice, then acts alone for the rest of the tool loop.
# "per_iteration" re-runs the advisors whenever the advisory view
# changes — i.e. every tool iteration, so advice tracks live task
# state at the cost of multiplying advisor spend by tool-loop depth.
# "every_n:<N>" (N >= 2) is the middle ground: advisors run on the
# first iteration of each user turn and every Nth tool iteration
# after it; in-between iterations reuse the cached guidance from the
# last advisor run. Also accepts the mapping form
# {mode: every_n, n: N}, normalized to the canonical string.
# When the reference fan-out runs. "per_iteration" (default) re-runs
# the advisors whenever the advisory view changes — i.e. every tool
# iteration, so advice tracks live task state. "user_turn" runs the
# advisors ONCE per user turn (the original MoA shape): the
# aggregator gets their upfront plan-level advice, then acts alone
# for the rest of the tool loop.
"fanout": _coerce_fanout(raw.get("fanout")),
}
@@ -410,16 +292,10 @@ def normalize_moa_config(raw: Any) -> dict[str, Any]:
"aggregator": deepcopy(active["aggregator"]),
"reference_temperature": active["reference_temperature"],
"aggregator_temperature": active["aggregator_temperature"],
"reference_timeout": active["reference_timeout"],
"degraded_reference_policy": active["degraded_reference_policy"],
"max_tokens": active["max_tokens"],
"reference_max_tokens": active.get("reference_max_tokens"),
"fanout": active.get("fanout", "user_turn"),
"fanout": active.get("fanout", "per_iteration"),
"enabled": active["enabled"],
# MoA-level (not per-preset) toggles ride at the top level alongside
# save_traces. privacy_filter: '' (off, default) | 'display' | 'full'
# — see coerce_privacy_filter for the semantics of each mode.
"privacy_filter": coerce_privacy_filter(raw.get("privacy_filter")),
}
+1 -3
View File
@@ -475,9 +475,7 @@ TOOL_CATEGORIES = {
"setup_title": "Select xAI Credential Source",
"setup_note": (
"Hermes routes X searches through xAI's built-in x_search "
"Responses tool for read-only public X discovery. Use the xurl "
"skill for authenticated X API reads and account actions. Both "
"credential sources hit the same "
"Responses tool. Both credential sources hit the same "
"https://api.x.ai/v1/responses endpoint — pick whichever you "
"already have. SuperGrok OAuth is preferred when both are set "
"(uses your subscription quota instead of API spend)."
+6 -36
View File
@@ -27,7 +27,6 @@ import inspect
import importlib.util
import json
import logging
import math
import mimetypes
import os
import queue
@@ -48,7 +47,7 @@ import zipfile
from hermes_cli._subprocess_compat import windows_detach_flags, windows_hide_flags
import urllib.request
from pathlib import Path
from typing import Any, Dict, List, Literal, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple
import yaml
@@ -106,7 +105,7 @@ try:
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, SecretStr, field_validator
from pydantic import BaseModel, SecretStr
from starlette.concurrency import run_in_threadpool
except ImportError:
# First try lazy-installing the dashboard extras. Only the user actually
@@ -122,7 +121,7 @@ except ImportError:
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, SecretStr, field_validator
from pydantic import BaseModel, SecretStr
from starlette.concurrency import run_in_threadpool
except Exception:
raise SystemExit(
@@ -1361,35 +1360,9 @@ class MoaModelSlot(BaseModel):
# Optional per-slot reasoning effort. Declared so a client round-tripping
# the GET payload doesn't have it stripped at parse time and wiped on save.
reasoning_effort: Optional[str] = None
enabled: bool = True
class _MoaReferenceControls(BaseModel):
# None = no per-preset override; the fan-out inherits
# auxiliary.moa_reference.timeout (900s default).
reference_timeout: Optional[float] = None
degraded_reference_policy: Literal["loud", "silent"] = "loud"
@field_validator("reference_timeout", mode="before")
@classmethod
def _validate_reference_timeout(cls, value: Any) -> Optional[float]:
"""Reject JSON booleans/non-finite values before float coercion."""
if value is None or value == "":
return None
if isinstance(value, bool):
raise ValueError("reference_timeout must be a finite positive number")
try:
timeout = float(value)
except (TypeError, ValueError) as exc:
raise ValueError(
"reference_timeout must be a finite positive number"
) from exc
if not math.isfinite(timeout) or timeout <= 0:
raise ValueError("reference_timeout must be a finite positive number")
return timeout
class MoaPresetPayload(_MoaReferenceControls):
class MoaPresetPayload(BaseModel):
reference_models: list[MoaModelSlot] = []
aggregator: MoaModelSlot = MoaModelSlot()
# None = temperature omitted from API calls (provider default), matching
@@ -1405,7 +1378,7 @@ class MoaPresetPayload(_MoaReferenceControls):
enabled: bool = True
class MoaConfigPayload(_MoaReferenceControls):
class MoaConfigPayload(BaseModel):
default_preset: str = "default"
active_preset: str = ""
presets: dict[str, MoaPresetPayload] = {}
@@ -6810,8 +6783,6 @@ def set_moa_models(body: MoaConfigPayload, profile: Optional[str] = None):
"aggregator": _slot_dict(preset.aggregator),
"reference_temperature": preset.reference_temperature,
"aggregator_temperature": preset.aggregator_temperature,
"reference_timeout": preset.reference_timeout,
"degraded_reference_policy": preset.degraded_reference_policy,
"max_tokens": preset.max_tokens,
"reference_max_tokens": preset.reference_max_tokens,
"fanout": preset.fanout,
@@ -6833,8 +6804,6 @@ def set_moa_models(body: MoaConfigPayload, profile: Optional[str] = None):
aggregator=body.aggregator,
reference_temperature=body.reference_temperature,
aggregator_temperature=body.aggregator_temperature,
reference_timeout=body.reference_timeout,
degraded_reference_policy=body.degraded_reference_policy,
max_tokens=body.max_tokens,
reference_max_tokens=body.reference_max_tokens,
fanout=body.fanout,
@@ -6854,6 +6823,7 @@ def set_moa_models(body: MoaConfigPayload, profile: Optional[str] = None):
status_code=422,
detail="Invalid MoA config: " + "; ".join(problems),
)
normalized = normalize_moa_config(raw)
# Merge instead of overwrite so that hand-edited keys not declared
# in MoaConfigPayload (e.g. save_traces, trace_dir) survive a GUI
@@ -1,6 +1,6 @@
---
name: blackbox
description: Delegate coding tasks to the Blackbox AI multi-model CLI.
description: Delegate coding tasks to Blackbox AI CLI agent. Multi-model agent with built-in judge that runs tasks through multiple LLMs and picks the best result. Requires the blackbox CLI and a Blackbox AI API key.
version: 1.0.0
author: Hermes Agent (Nous Research)
license: MIT
@@ -1,6 +1,6 @@
---
name: honcho
description: Configure and troubleshoot Honcho memory for Hermes.
description: Configure and use Honcho memory with Hermes -- cross-session user modeling, multi-profile peer isolation, observation config, dialectic reasoning, session summaries, and context budget enforcement. Use when setting up Honcho, troubleshooting memory, managing profiles with Honcho peers, or tuning observation, recall, and dialectic settings.
version: 2.0.0
author: Hermes Agent
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: solana
description: Query Solana wallets, tokens, txs, and NFTs in USD.
description: Query Solana blockchain data with USD pricing — wallet balances, token portfolios with values, transaction details, NFTs, whale detection, and live network stats. Uses Solana RPC + CoinGecko. No API key required.
version: 0.2.0
author: Deniz Alagoz (gizdusum), enhanced by Hermes Agent
license: MIT
@@ -1,6 +1,6 @@
---
name: concept-diagrams
description: Generate flat, minimal educational SVG visuals as HTML.
description: Generate flat, minimal light/dark-aware SVG diagrams as standalone HTML files, using a unified educational visual language with 9 semantic color ramps, sentence-case typography, and automatic dark mode. Best suited for educational and non-software visuals — physics setups, chemistry mechanisms, math curves, physical objects (aircraft, turbines, smartphones, mechanical watches), anatomy, floor plans, cross-sections, narrative journeys (lifecycle of X, process of Y), hub-spoke system integrations (smart city, IoT), and exploded layer views. If a more specialized skill exists for the subject (dedicated software/cloud architecture, hand-drawn sketches, animated explainers, etc.), prefer that — otherwise this skill can also serve as a general-purpose SVG diagram fallback with a clean educational look. Ships with 15 example diagrams.
version: 0.1.0
author: v1k22 (original PR), ported into hermes-agent
license: MIT
@@ -1,6 +1,6 @@
---
name: hyperframes
description: Render MP4/WebM videos from HTML compositions.
description: Create HTML-based video compositions, animated title cards, social overlays, captioned talking-head videos, audio-reactive visuals, and shader transitions using HyperFrames. HTML is the source of truth for video. Use when the user wants a rendered MP4/WebM from an HTML composition, wants to animate text/logos/charts over media, needs captions synced to audio, wants TTS narration, or wants to convert a website into a video.
version: 1.0.0
author: heygen-com
license: Apache-2.0
@@ -1,6 +1,6 @@
---
name: kanban-video-orchestrator
description: Plan and run multi-agent video production pipelines.
description: Plan, set up, and monitor a multi-agent video production pipeline backed by Hermes Kanban. Use when the user wants to make ANY video — narrative film, product/marketing, music video, explainer, ASCII/terminal art, abstract/generative loop, comic, 3D, real-time/installation — and the work warrants decomposition into specialized profiles (writer, designer, animator, renderer, voice, editor, etc.) coordinated through a kanban board. Performs adaptive discovery to scope the brief, designs an appropriate team for the requested style, generates the setup script that creates Hermes profiles + initial kanban task, then helps monitor execution and intervene when tasks stall or fail. Routes scenes to whichever Hermes rendering / audio / design skill fits each beat (`ascii-video`, `manim-video`, `p5js`, `comfyui`, `touchdesigner-mcp`, `blender-mcp`, `pixel-art`, `baoyu-comic`, `claude-design`, `excalidraw`, `songsee`, `heartmula`, …) plus external APIs for TTS, image-gen, and image-to-video as needed.
version: 1.0.0
author: [SHL0MS, alt-glitch]
license: MIT
@@ -1,6 +1,6 @@
---
name: meme-generation
description: Create meme PNGs from templates with Pillow text overlay.
description: Generate real meme images by picking a template and overlaying text with Pillow. Produces actual .png meme files.
version: 2.0.0
author: adanaleycio
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: unreal-mcp
description: Automate Unreal Engine editor scenes, actors, and renders.
description: "Use when the user wants to do anything in Unreal Engine through Epic's official editor-embedded MCP server (catalog entry: unreal-engine) — build/light/populate scenes, place and transform actors, author Blueprints, animate with Sequencer, create material instances, frame cameras, take screenshots, render, import assets, run PIE test sessions and automation tests, or automate the editor end-to-end from plain-English prompts with no Unreal knowledge required. Covers the tool-search discovery walk (list_toolsets/describe_toolset/call_tool), serial game-thread call discipline, ProgrammaticToolset batching, the Blueprint graph DSL loop, scene-craft numbers (physical light units, exposure, scale conventions), complete build recipes, save/undo hygiene, and extending the tool surface with custom Python toolsets."
version: 1.0.0
requires: Unreal Editor 5.8+ with the Unreal MCP plugin enabled and its server running
author: Hermes Agent
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: inference-sh-cli
description: Run 150+ AI apps (image, video, LLM) via inference.sh CLI.
description: "Run 150+ AI apps via inference.sh CLI (infsh) — image generation, video creation, LLMs, search, 3D, social automation. Uses the terminal tool. Triggers: inference.sh, infsh, ai apps, flux, veo, image generation, video generation, seedream, seedance, tavily"
version: 1.0.0
author: okaris
license: MIT
@@ -1,6 +1,6 @@
---
name: docker-management
description: Manage Docker containers, images, volumes, and Compose.
description: Manage Docker containers, images, volumes, networks, and Compose stacks — lifecycle ops, debugging, cleanup, and Dockerfile optimization.
version: 1.0.0
author: sprmn24
license: MIT
@@ -1,6 +1,6 @@
---
name: hermes-s6-container-supervision
description: Modify or debug s6 services in the Hermes Docker image.
description: Modify, debug, or extend the s6-overlay supervision tree inside the Hermes Agent Docker image — adding new services, debugging profile gateways, understanding the Architecture B main-program pattern.
version: 1.0.0
author: Hermes Agent
license: MIT
@@ -1,6 +1,6 @@
---
name: adversarial-ux-test
description: Roleplay a hostile user to find and triage UX pain points.
description: Roleplay the most difficult, tech-resistant user for your product. Browse the app as that persona, find every UX pain point, then filter complaints through a pragmatism layer to separate real problems from noise. Creates actionable tickets from genuine issues only.
version: 1.0.0
author: Omni @ Comelse
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: agentmail
description: "Give the agent its own inbox: send and receive email."
description: Give the agent its own dedicated email inbox via AgentMail. Send, receive, and manage email autonomously using agent-owned email addresses (e.g. hermes-agent@agentmail.to).
version: 1.0.0
platforms: [linux, macos, windows]
metadata:
@@ -1,6 +1,6 @@
---
name: 3-statement-model
description: Build integrated IS/BS/CF financial workbooks in Excel.
description: Build fully-integrated 3-statement models (IS, BS, CF) in Excel with working capital schedules, D&A roll-forwards, debt schedule, and the plugs that make cash and retained earnings tie. Pairs with excel-author.
version: 1.0.0
author: Anthropic (adapted by Nous Research)
license: Apache-2.0
@@ -1,6 +1,6 @@
---
name: comps-analysis
description: Build comparable-company valuation workbooks in Excel.
description: Build comparable company analysis in Excel — operating metrics, valuation multiples, statistical benchmarking vs peer sets. Pairs with excel-author. Use for public-company valuation, IPO pricing, sector benchmarking, or outlier detection.
version: 1.0.0
author: Anthropic (adapted by Nous Research)
license: Apache-2.0
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: dcf-model
description: Build discounted cash flow valuation workbooks in Excel.
description: Build institutional-quality DCF valuation models in Excel — revenue projections, FCF build, WACC, terminal value, Bear/Base/Bull scenarios, 5x5 sensitivity tables. Pairs with excel-author. Use for intrinsic-value equity analysis.
version: 1.0.0
author: Anthropic (adapted by Nous Research)
license: Apache-2.0
@@ -1,6 +1,6 @@
---
name: excel-author
description: Build auditable financial workbooks headless via openpyxl.
description: Build auditable Excel workbooks headless with openpyxl — blue/black/green cell conventions, formulas over hardcodes, named ranges, balance checks, sensitivity tables. Use for financial models, audit outputs, reconciliations.
version: 1.0.0
author: Anthropic (adapted by Nous Research)
license: Apache-2.0
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: lbo-model
description: Build leveraged buyout workbooks with IRR/MOIC in Excel.
description: Build leveraged buyout models in Excel — sources & uses, debt schedule, cash sweep, exit multiple, IRR/MOIC sensitivity. Pairs with excel-author. Use for PE screening, sponsor-case valuation, or illustrative LBO in a pitch.
version: 1.0.0
author: Anthropic (adapted by Nous Research)
license: Apache-2.0
@@ -1,6 +1,6 @@
---
name: merger-model
description: Build M&A accretion/dilution workbooks in Excel.
description: Build accretion/dilution (merger) models in Excel — pro-forma P&L, synergies, financing mix, EPS impact. Pairs with excel-author. Use for M&A pitches, board materials, or deal evaluation.
version: 1.0.0
author: Anthropic (adapted by Nous Research)
license: Apache-2.0
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: pptx-author
description: Build PowerPoint decks headless with python-pptx.
description: Build PowerPoint decks headless with python-pptx. Pairs with excel-author for model-backed decks where every number traces to a workbook cell. Use for pitch decks, IC memos, earnings notes.
version: 1.0.0
author: Anthropic (adapted by Nous Research)
license: Apache-2.0
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: fastmcp
description: Build, test, and deploy Python MCP servers.
description: Build, test, inspect, install, and deploy MCP servers with FastMCP in Python. Use when creating a new MCP server, wrapping an API or database as MCP tools, exposing resources or prompts, or preparing a FastMCP server for Claude Code, Cursor, or HTTP deployment.
version: 1.0.0
author: Hermes Agent
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: mcporter
description: List, auth, and call MCP servers/tools from the terminal.
description: Use the mcporter CLI to list, configure, auth, and call MCP servers/tools directly (HTTP or stdio), including ad-hoc servers, config edits, and CLI/type generation.
version: 1.0.0
author: community
license: MIT
@@ -1,6 +1,6 @@
---
name: openclaw-migration
description: Import an OpenClaw setup (memories, skills) into Hermes.
description: Migrate a user's OpenClaw customization footprint into Hermes Agent. Imports Hermes-compatible memories, SOUL.md, command allowlists, user skills, and selected workspace assets from ~/.openclaw, then reports exactly what could not be migrated and why.
version: 1.0.0
author: Hermes Agent (Nous Research)
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: huggingface-accelerate
description: Run PyTorch training across GPUs with minimal changes.
description: Simplest distributed training API. 4 lines to add distributed support to any PyTorch script. Unified API for DeepSpeed/FSDP/Megatron/DDP. Automatic device placement, mixed precision (FP16/BF16/FP8). Interactive config, single launch command. HuggingFace ecosystem standard.
version: 1.0.0
author: Orchestra Research
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: chroma
description: Embedding database for RAG and semantic search.
description: Open-source embedding database for AI applications. Store embeddings and metadata, perform vector and full-text search, filter by metadata. Simple 4-function API. Scales from notebooks to production clusters. Use for semantic search, RAG applications, or document retrieval. Best for local development and open-source projects.
version: 1.0.0
author: Orchestra Research
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: clip
description: Zero-shot image classification and image-text search.
description: OpenAI's model connecting vision and language. Enables zero-shot image classification, image-text matching, and cross-modal retrieval. Trained on 400M image-text pairs. Use for image search, content moderation, or vision-language tasks without fine-tuning. Best for general-purpose image understanding.
version: 1.0.0
author: Orchestra Research
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: faiss
description: Fast vector similarity search at billion scale.
description: Facebook's library for efficient similarity search and clustering of dense vectors. Supports billions of vectors, GPU acceleration, and various index types (Flat, IVF, HNSW). Use for fast k-NN search, large-scale vector retrieval, or when you need pure similarity search without metadata. Best for high-performance applications.
version: 1.0.0
author: Orchestra Research
license: MIT
@@ -1,6 +1,6 @@
---
name: optimizing-attention-flash
description: Speed up long-sequence transformer training and inference.
description: Optimizes transformer attention with Flash Attention for 2-4x speedup and 10-20x memory reduction. Use when training/running transformers with long sequences (>512 tokens), encountering GPU memory issues with attention, or need faster inference. Supports PyTorch native SDPA, flash-attn library, H100 FP8, and sliding window attention.
version: 1.0.0
author: Orchestra Research
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: guidance
description: Constrain LLM output with grammars; guarantee valid JSON.
description: Control LLM output with regex and grammars, guarantee valid JSON/XML/code generation, enforce structured formats, and build multi-step workflows with Guidance - Microsoft Research's constrained generation framework
version: 1.0.0
author: Orchestra Research
license: MIT
@@ -1,6 +1,6 @@
---
name: huggingface-tokenizers
description: Fast BPE/WordPiece tokenization and custom vocab training.
description: Fast tokenizers optimized for research and production. Rust-based implementation tokenizes 1GB in <20 seconds. Supports BPE, WordPiece, and Unigram algorithms. Train custom vocabularies, track alignments, handle padding/truncation. Integrates seamlessly with transformers. Use when you need high-performance tokenization or custom tokenizer training.
version: 1.0.0
author: Orchestra Research
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: instructor
description: Structured LLM outputs validated with Pydantic.
description: Extract structured data from LLM responses with Pydantic validation, retry failed extractions automatically, parse complex JSON with type safety, and stream partial results with Instructor - battle-tested structured output library
version: 1.0.0
author: Orchestra Research
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: lambda-labs-gpu-cloud
description: On-demand GPU cloud instances for ML training.
description: Reserved and on-demand GPU cloud instances for ML training and inference. Use when you need dedicated GPU instances with simple SSH access, persistent filesystems, or high-performance multi-node clusters for large-scale training.
version: 1.0.0
author: Orchestra Research
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: llava
description: "Vision-language chat: VQA, captioning, image dialogue."
description: Large Language and Vision Assistant. Enables visual instruction tuning and image-based conversations. Combines CLIP vision encoder with Vicuna/LLaMA language models. Supports multi-turn image chat, visual question answering, and instruction following. Use for vision-language chatbots or image understanding tasks. Best for conversational image analysis.
version: 1.0.0
author: Orchestra Research
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: modal-serverless-gpu
description: Serverless GPU cloud for ML jobs and model APIs.
description: Serverless GPU cloud platform for running ML workloads. Use when you need on-demand GPU access without infrastructure management, deploying ML models as APIs, or running batch jobs with automatic scaling.
version: 1.0.0
author: Orchestra Research
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: nemo-curator
description: "Curate LLM training data: dedupe, filter, PII redaction."
description: GPU-accelerated data curation for LLM training. Supports text/image/video/audio. Features fuzzy deduplication (16× faster), quality filtering (30+ heuristics), semantic deduplication, PII redaction, NSFW detection. Scales across GPUs with RAPIDS. Use for preparing high-quality training datasets, cleaning web data, or deduplicating large corpora.
version: 1.0.0
author: Orchestra Research
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: peft-fine-tuning
description: Fine-tune large LLMs with LoRA on limited GPU memory.
description: Parameter-efficient fine-tuning for LLMs using LoRA, QLoRA, and 25+ methods. Use when fine-tuning large models (7B-70B) with limited GPU memory, when you need to train <1% of parameters with minimal accuracy loss, or for multi-adapter serving. HuggingFace's official library integrated with transformers ecosystem.
version: 1.0.0
author: Orchestra Research
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: pinecone
description: Managed vector DB for production RAG and search.
description: Managed vector database for production AI applications. Fully managed, auto-scaling, with hybrid search (dense + sparse), metadata filtering, and namespaces. Low latency (<100ms p95). Use for production RAG, recommendation systems, or semantic search at scale. Best for serverless, managed infrastructure.
version: 1.0.0
author: Orchestra Research
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: pytorch-fsdp
description: Fully sharded data-parallel training for large models.
description: Expert guidance for Fully Sharded Data Parallel training with PyTorch FSDP - parameter sharding, mixed precision, CPU offloading, FSDP2
version: 1.0.0
author: Orchestra Research
license: MIT
@@ -1,6 +1,6 @@
---
name: pytorch-lightning
description: Clean training loops with built-in distributed support.
description: High-level PyTorch framework with Trainer class, automatic distributed training (DDP/FSDP/DeepSpeed), callbacks system, and minimal boilerplate. Scales from laptop to supercomputer with same code. Use when you want clean training loops with built-in best practices.
version: 1.0.0
author: Orchestra Research
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: qdrant-vector-search
description: Vector search engine for production RAG systems.
description: High-performance vector similarity search engine for RAG and semantic search. Use when building production RAG systems requiring fast nearest neighbor search, hybrid search with filtering, or scalable vector storage with Rust-powered performance.
version: 1.0.0
author: Orchestra Research
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: sparse-autoencoder-training
description: Train sparse autoencoders to interpret model features.
description: Provides guidance for training and analyzing Sparse Autoencoders (SAEs) using SAELens to decompose neural network activations into interpretable features. Use when discovering interpretable features, analyzing superposition, or studying monosemantic representations in language models.
version: 1.0.0
author: Orchestra Research
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: simpo-training
description: Reference-free preference alignment, simpler than DPO.
description: Simple Preference Optimization for LLM alignment. Reference-free alternative to DPO with better performance (+6.4 points on AlpacaEval 2.0). No reference model needed, more efficient than DPO. Use for preference alignment when want simpler, faster training than DPO/PPO.
version: 1.0.0
author: Orchestra Research
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: slime-rl-training
description: RL post-training for LLMs with Megatron and SGLang.
description: Provides guidance for LLM post-training with RL using slime, a Megatron+SGLang framework. Use when training GLM models, implementing custom data generation workflows, or needing tight Megatron-LM integration for RL scaling.
version: 1.0.0
author: Orchestra Research
license: MIT
@@ -1,6 +1,6 @@
---
name: stable-diffusion-image-generation
description: Text-to-image generation, inpainting, and img2img.
description: State-of-the-art text-to-image generation with Stable Diffusion models via HuggingFace Diffusers. Use when generating images from text prompts, performing image-to-image translation, inpainting, or building custom diffusion pipelines.
version: 1.0.0
author: Orchestra Research
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: tensorrt-llm
description: High-throughput LLM inference on NVIDIA GPUs.
description: Optimizes LLM inference with NVIDIA TensorRT for maximum throughput and lowest latency. Use for production deployment on NVIDIA GPUs (A100/H100), when you need 10-100x faster inference than PyTorch, or for serving models with quantization (FP8/INT4), in-flight batching, and multi-GPU scaling.
version: 1.0.0
author: Orchestra Research
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: distributed-llm-pretraining-torchtitan
description: Pretrain LLMs at scale with PyTorch 4D parallelism.
description: Provides PyTorch-native distributed LLM pretraining using torchtitan with 4D parallelism (FSDP2, TP, PP, CP). Use when pretraining Llama 3.1, DeepSeek V3, or custom models at scale from 8 to 512+ GPUs with Float8, torch.compile, and distributed checkpointing.
version: 1.0.0
author: Orchestra Research
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: whisper
description: Transcribe and translate speech in 99 languages.
description: OpenAI's general-purpose speech recognition model. Supports 99 languages, transcription, translation to English, and language identification. Six model sizes from tiny (39M params) to large (1550M params). Use for speech-to-text, podcast transcription, or multilingual audio processing. Best for robust, multilingual ASR.
version: 1.0.0
author: Orchestra Research
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: canvas
description: Fetch Canvas LMS courses and assignments via API token.
description: Canvas LMS integration — fetch enrolled courses and assignments using API token authentication.
version: 1.0.0
author: community
license: MIT
@@ -1,6 +1,6 @@
---
name: here.now
description: Publish sites to {slug}.here.now and store files in Drives.
description: Publish static sites to {slug}.here.now and store private files in cloud Drives for agent-to-agent handoff.
version: 1.15.3
author: here.now
license: MIT
@@ -1,6 +1,6 @@
---
name: shopify
description: Query Shopify Admin/Storefront GraphQL APIs via curl.
description: Shopify Admin & Storefront GraphQL APIs via curl. Products, orders, customers, inventory, metafields.
version: 1.0.0
author: community
license: MIT
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: siyuan
description: Query and edit a SiYuan knowledge base via its API.
description: SiYuan Note API for searching, reading, creating, and managing blocks and documents in a self-hosted knowledge base via curl.
version: 1.0.0
author: FEUAZUR
license: MIT

Some files were not shown because too many files have changed in this diff Show More