Commit graph

1878 commits

Author SHA1 Message Date
liuwei666888
dd923619c7 fix: prevent TypeError in _summarize_tool_result when tool args contain non-string values
When LLMs return non-string parameter values (e.g. bool, int) in tool call
arguments, _summarize_tool_result() crashes with TypeError because it calls
len(), .count(), or slicing directly on args.get() return values.

This causes an infinite crash loop in the TUI — context compression
triggers on session resume, which crashes, which restarts, which triggers
compression again.

Add _str_arg() helper that coerces any value to str, and use it in all 5
vulnerable call sites:
- terminal: len(cmd)
- write_file: content.count()
- delegate_task: len(goal)
- execute_code: len(code)
- vision_analyze: question[:50]
2026-07-15 00:10:22 -07:00
kshitijk4poor
5cbbade24c fix(streaming): zero-event guard parity for the anthropic_messages path
The chat_completions path raises EmptyStreamError when a stream yields
no chunks and no finish_reason (#64420). _call_anthropic() had no
equivalent, and the eventless failure surfaces differently by client:

- Real Anthropic SDK: no message_start means no final-message snapshot,
  so get_final_message() raises a bare AssertionError — not in the
  retry loop's transient set, so it burned no retries and surfaced raw.
- OpenAI-compat shims: may fabricate a contentless Message with no
  stop_reason (or return None), flowing out as a 'successful' empty turn.

Track whether any stream event arrived and normalize both shapes to
EmptyStreamError, giving the anthropic path the same transient retry
budget in the shared _call() retry loop. A real completed response
always carries a stop_reason, so the guard cannot fire on legitimate
turns (including eventless mocks with stop_reason='end_turn').

Follow-up to #64420.
2026-07-15 10:58:36 +05:30
kshitijk4poor
92057474f3 fix(streaming): distinguish empty-stream exhaustion from connection failure in status message
An exhausted EmptyStreamError previously reported 'Connection to provider
failed' — misleading, since the connection succeeded (stream opened) and
the provider simply sent nothing. Add a dedicated third branch so users
debugging a misconfigured endpoint aren't sent chasing network issues.

Follow-up to #64420.
2026-07-15 10:58:36 +05:30
Simplelife
f4af35f90d fix(streaming): retry zero-chunk streams 2026-07-15 10:58:36 +05:30
Teknium
e12626b34f fix: adapt null-args salvage to segment planner, align tests with current contracts
Follow-up to @michaelHMK's cherry-picked fix for #50892:

- agent/tool_dispatch_helpers.py: guard the segment planner's except-path
  debug log against non-string arguments (the planner replaced the old
  _should_parallelize_tool_batch body after #64460 and inherited the same
  latent arguments[:200] slice).
- tests: the PR's executor-coercion hunks were dropped as redundant —
  both executors now route through _parse_tool_arguments, which already
  rejects null/non-object args with a structured error result instead of
  coercing to {} (the 'we do not repair bad model outputs' contract).
  Reworked the salvaged tests to pin the current behavior: None args are
  rejected without dispatch, valid siblings still run, the planner treats
  them as a barrier without raising, and the mainline run_conversation
  path (which normalizes None to '{}' before dispatch) stays crash-free
  under verbose logging.
2026-07-14 21:46:41 -07:00
Michael Huang
94456a1288 Handle null tool call arguments 2026-07-14 21:46:41 -07:00
Teknium
f0a8e45cdc chore(mcp): drop stale input_schema comment on add_tool call
Review nit from verification: the comment claimed newer FastMCP accepts a
JSON schema kwarg — the installed SDK's add_tool has no such parameter; the
synthesized __signature__ is what drives schema generation on both paths.
2026-07-14 21:36:32 -07:00
liuhao1024
3ad5876feb fix(mcp): pass params_schema to MCP tools via Python signatures
Fixes #64025

The hermes-tools MCP server was fetching each tool's JSON schema
into params_schema but never passing it to FastMCP's add_tool(),
so all published tools had an empty **kwargs signature. MCP clients
couldn't see parameters and arguments were dropped at dispatch.

This fix:
- Adds _signature_from_schema() to convert JSON schemas to Python
  function signatures with type annotations
- Attaches the generated signature/annotations to each handler closure
  so FastMCP introspects the real parameter structure
- Filters out None values before dispatch to avoid forwarding unset
  optional parameters

Impact: web_search, browser automation, vision, and other Hermes tools
are now properly callable from the codex_app_server runtime.
2026-07-14 21:36:32 -07:00
webtecnica
398cf40c08 fix(nous): restore inference-api.nousresearch.com base_url
The upstream migration to inference.nousresearch.com broke routing for
inference-api.nousresearch.com. Restore the correct base_url and drop
the stale alias match in _is_nous_inference_route().

Fixes #60715
2026-07-14 21:31:04 -07:00
Teknium
0bb3a82c53 refactor(moa): drop auxiliary-task reasoning knob in favor of per-slot preset config
The just-merged auxiliary.<task>.reasoning_effort shorthand applied
ensemble-wide to MoA (one value for every advisor) — wrong granularity.
Per-slot preset config supersedes it:

  moa:
    presets:
      deep_review:
        reference_models:
          - {provider: ..., model: ..., reasoning_effort: low}
          - {provider: ..., model: ..., reasoning_effort: xhigh}
        aggregator:
          {provider: ..., model: ..., reasoning_effort: high}

- Remove reasoning_effort from the moa_reference/moa_aggregator
  DEFAULT_CONFIG blocks; _get_task_extra_body now warns-and-ignores the
  key on MoA tasks, pointing at the preset config
- Guard tests: MoA aux blocks must not regrow the key; task-level value
  is rejected with the pointer warning
- Docs: configuration.md notes the MoA exception and links the MoA page
2026-07-14 21:08:22 -07:00
Justin Schille
5646dbdd5b fix(moa): project slot reasoning through provider profiles 2026-07-14 21:08:22 -07:00
Justin Schille
3dca75b45c feat(moa): support per-slot reasoning effort 2026-07-14 21:08:22 -07:00
Teknium
df5700ebe3
feat(auxiliary): per-task reasoning_effort for auxiliary models (#64597)
Every auxiliary task block (vision, web_extract, compression,
title_generation, curator, background_review, moa_reference, ...) now
accepts a reasoning_effort shorthand:

  auxiliary:
    compression:
      reasoning_effort: low
    vision:
      reasoning_effort: none

_get_task_extra_body() folds it into extra_body.reasoning, which every
auxiliary wire already translates: chat.completions passes it through,
the Codex Responses adapter maps it to top-level reasoning/include, and
the Anthropic auxiliary adapter now forwards it into
build_anthropic_kwargs(reasoning_config=...) (previously hardcoded None).

An explicit extra_body.reasoning on the same task wins over the
shorthand. Invalid levels are ignored with a warning. Empty string
(the shipped default) is a no-op — zero behavior change.

Config: reasoning_effort added to all 16 auxiliary task blocks in
DEFAULT_CONFIG (no version bump — deep-merge handles new keys).
2026-07-14 14:07:43 -07:00
Teknium
271a9d8ec6
perf(agent): segment mixed tool batches to recover lost concurrency (#64460)
A model response containing several parallel-safe reads plus one unsafe
tool used to lose ALL concurrency: _should_parallelize_tool_batch was
all-or-nothing, so a single barrier call (terminal, clarify, unknown
tool, malformed args) forced the entire batch onto the sequential path.

_plan_tool_batch_segments now splits the batch into ordered segments:
maximal contiguous runs of parallel-safe calls execute on the existing
concurrent path, barrier calls on the sequential path, strictly in the
model's emission order. Invariants preserved:

- one tool result per call, appended in emission order (segments are
  contiguous, so no result reordering across a barrier)
- side-effect boundaries: no call starts before an earlier barrier ends
- overlapping file targets split into separate ordered parallel runs
- turn-end budget enforcement + /steer injection run exactly once per
  batch (segment executors run with finalize=False; the segmented
  dispatcher owns the whole-turn finalize)
- interrupt during segment k drains segments k+1..n with cancelled
  results, keeping one result per tool_call_id

Homogeneous batches keep their original single-path dispatch (zero
behavior delta); _should_parallelize_tool_batch remains as a thin view
over the planner for existing callers and tests.
2026-07-14 11:53:05 -07:00
Teknium
e81d18dfb4 refactor(reasoning): unify per-model reasoning resolution behind a single chokepoint
Collapse the six per-surface copies of override-then-global resolution
(CLI startup, gateway, TUI, cron, /model switch, fallback activation)
onto one shared resolve_reasoning_config() in hermes_constants.

Also fixes the gateway resolving reasoning against config model.default
instead of the session's effective model: after a session-only /model
switch, the switched model's override now applies (gateway message paths
pass the resolved session model through _resolve_session_reasoning_config;
/reasoning status reads the session model override).

Cleanup: drop docs/PER_MODEL_REASONING.md (duplicates the website docs
page), drop the change-detector _config_version test (no bump needed —
deep-merge handles new keys), remove a stale plan-reference comment.

Adds chokepoint contract tests (13) and gateway session-effective-model
regression tests (2).
2026-07-14 11:46:40 -07:00
ScotterMonk
d9cdb81923 feat(config): support per-model reasoning_effort overrides
Add agent.reasoning_overrides dict to config.yaml. Users can now set
a reasoning_effort per model, overriding the global agent.reasoning_effort.

Example:
  agent:
    reasoning_effort: "medium"       # global default
    reasoning_overrides:
      "openrouter/anthropic/claude-opus-4.5": "xhigh"
      "openai/gpt-5": "low"
      "claude-sonnet-4.6": "high"    # bare model name also works

The helper is spelling-tolerant: override keys match regardless of
provider prefix or dots-vs-dashes normalization, so users can write
keys in any sensible form and they'll match.

Resolution priority:
1. Session-scoped /reasoning --session override (gateway only; unchanged)
2. Per-model override from agent.reasoning_overrides (spelling-tolerant)
3. Global agent.reasoning_effort (existing)
4. Provider default (unchanged)

Wired into:
- CLI startup (cli.py)
- Messaging gateway agent construction (gateway/run.py)
- Desktop/TUI _load_reasoning_config (tui_gateway/server.py)
- Cron job scheduler (cron/scheduler.py)
- /model mid-session switch (agent/agent_runtime_helpers.py)
  + _primary_runtime now tracks reasoning_config for correct fallback recovery
- Fallback activation (agent/chat_completion_helpers.py::try_activate_fallback)
  + Re-resolves reasoning_config for the fallback model (best-effort)

Closes #21256 (per-model reasoning_effort defaults).

Note: no hermes config set agent.reasoning_overrides.<model> support;
users edit the YAML directly. _set_nested splits on "." and would
corrupt model keys containing version dots.
2026-07-14 11:46:40 -07:00
Changhyun Min
35d3fc3b09 refactor(agent): drop the solar-pro rolling alias, default to solar-pro3
Pin the Upstage default to the concrete solar-pro3 instead of the
solar-pro rolling alias:
- plugin fallback_models is now ("solar-pro3",); entry [0] is the setup default
- drop the "solar-pro" context-window fallback entry (solar-pro3 covers it)
- update the reasoning default-on docstring and profile tests accordingly

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 00:09:24 +05:30
Changhyun Min
5f3d57400b refactor(agent): drop solar-open2-preview from Solar context fallbacks
Remove the `solar-open2-preview` context-window entry; `solar-open2`
covers the Open 2 family at the same 256K window.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 00:09:24 +05:30
Changhyun Min
20502b407c feat(agent): add Upstage Solar as a model provider
Adds Upstage Solar as a bundled model-provider plugin. Solar exposes an
OpenAI-compatible chat-completions endpoint at https://api.upstage.ai/v1, so
the generic chat_completions transport handles request/response/streaming/tool
calls — the profile is the core integration.

Provider registration (Upstage isn't in models.dev, so each registry that does
not auto-wire from the plugin layer needs an explicit entry — same pattern as
nvidia/gmi):
- plugins/model-providers/upstage/: UpstageProfile + plugin.yaml. Picker default
  and offline catalog list only the agentic Solar Pro models, led by `solar-pro`
  (rolling alias for the latest Pro). default_aux_model empty so aux tasks use
  the main model. `solar` alias. UPSTAGE_BASE_URL overrides the host.
- hermes_cli/providers.py: HERMES_OVERLAYS + label + `solar` alias, so
  resolve_provider_full('upstage') resolves (without this, an explicit
  `provider: upstage` in config was dropped and fell through to auto-detect).
- hermes_cli/auth.py: PROVIDER_REGISTRY entry + `solar` alias, so `hermes
  doctor` / resolve_provider recognise upstage (the static-registry path the
  lazy profile-extension doesn't reliably cover at validation time).
- hermes_cli/models.py: CANONICAL_PROVIDERS entry places Upstage Solar in the
  curated picker order (above the auto-appended `custom`).
- agent/model_metadata.py: context-window fallbacks (/v1/models omits
  context_length); `solar-pro` carries the 128K Pro context as the catch-all.

Reasoning: UpstageProfile.build_api_kwargs_extras wires Solar's top-level
`reasoning_effort` (low|medium|high; xhigh/max→high). Reasoning-capable families
are solar-pro* and solar-open*; solar-mini/syn-pro never receive it. Defaults ON
at medium when unset (matches the /reasoning "medium (default)" label);
`/reasoning none` disables; explicit/saved settings are honored. No
reasoning_content echo handling needed (unlike DeepSeek/Kimi).

Web dashboard:
- web/src/pages/EnvPage.tsx: add an "Upstage Solar" provider group so
  UPSTAGE_API_KEY / UPSTAGE_BASE_URL appear under LLM Providers (not "Other").

Docs/tests:
- .env.example: documents UPSTAGE_API_KEY / UPSTAGE_BASE_URL.
- tests: profile wiring, reasoning_effort mapping (pro/open/mini, efforts,
  disabled, default-on), provider-resolver regression (resolve_provider_full /
  get_provider / solar alias / overlay), `solar-pro` default.

Testing: pytest tests/providers tests/plugins/model_providers
tests/hermes_cli/test_upstage_provider.py tests/run_agent/test_provider_parity.py
tests/hermes_cli/test_api_key_providers.py; ruff clean. Verified end-to-end:
`hermes doctor` shows "Upstage Solar", and live chat works via both
`--provider upstage` and `--provider solar`. Reasoning wire format per
https://console.upstage.ai/api/docs/for-agents/raw. Platforms tested: macOS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 00:09:24 +05:30
Teknium
b013ed03e5 fix(moa): scope the non-text placeholder to structured content only
Follow-up to the cherry-picked empty-user-turn drop: the placeholder
introduced in 8582f35d9 fired for whitespace-only STRING turns too
(content='   ' flattens to non-stripping text but isn't in the
(None, '', []) exclusion set), fabricating an attachment note for a turn
that carried nothing. Gate the placeholder on isinstance(content, list)
so only genuinely structured (e.g. image-only) turns get it; empty and
whitespace-only string turns now fall through to the drop path.

Edge cases verified: trailing empty user turn still ends the view on the
synthetic advisory marker; an all-empty transcript degenerates to [].
2026-07-14 06:49:36 -07:00
neo
b4c2c4f922 fix: drop empty user turns from MoA advisory view (strict-provider 400)
MoA's _reference_messages() unconditionally appended every user-role
message to the advisory view sent to reference models, even when the
message content was an empty string or a non-string/multimodal payload
that the text-extraction step flattens to "".

Strict providers (Kimi/Moonshot, and others that enforce non-empty user
content) reject such a message with:

  400 Invalid request: the message at position N with role 'user'
      must not be empty

Lenient providers (DeepSeek) accept it, so an identical rendered view
passes on one reference and 400s on another within the same fan-out —
the user sees "kimi doesn't support MoA" when the real cause is an empty
user turn leaking into the advisory transcript.

Skip empty user turns, mirroring the existing behavior for empty
assistant turns (which are already dropped when they carry no parts).
The end-on-user invariant is preserved: the synthetic advisory-request
user turn is still appended when the view would otherwise end on an
assistant turn.

Adds a regression test asserting the advisory view contains no empty
user turn and still ends on a user turn.
2026-07-14 06:49:36 -07:00
liuhao1024
b45a217e0a fix(agent): gate Telegram rich-Markdown hint on rich_messages config
The platform hint in PLATFORM_HINTS['telegram'] always encouraged rich
Markdown constructs (tables, task lists, math, collapsible details) even
when rich_messages: false (the default). This caused the agent to produce
formatting that MarkdownV2 cannot render, especially broken on Telegram Web.

Split the hint into a base hint (MarkdownV2-compatible) and a
TELEGRAM_RICH_MESSAGES_HINT extension. The extension is conditionally
appended in system_prompt.py only when
platforms.telegram.extra.rich_messages is true.

Fixes #57122
2026-07-14 06:48:53 -07:00
kshitijk4poor
8ef006933e fix(background_review): gate reasoning_config inheritance on not-routed + dedupe recorder stubs
Review follow-up to the reasoning_config cache-parity fix:

- Only inherit the parent's reasoning_config when the fork runs on the
  parent's model (not routed). On the routed aux path
  (auxiliary.background_review.{provider,model}) the cache is cold
  regardless, so parity buys nothing, and the parent's effort vocabulary
  can be invalid for the routed model/provider: OpenRouter
  extra_body.reasoning.effort is forwarded unclamped
  (chat_completions.py) and codex_responses only maps max/ultra for
  gpt-5.6 — an exotic parent effort routed to a strict provider could
  400 the review. Mirrors the existing 'not _routed' gate on
  _cached_system_prompt / session_start three lines below.

- Add a routed-path regression test asserting reasoning_config is
  omitted from the fork kwargs when _resolve_review_runtime returns
  routed=True.

- Extract the four copy-pasted recorder stubs in
  test_background_review_cache_parity.py into a single
  _make_recorder_class() factory so a new fork attribute needs one stub
  edit, not four.
2026-07-14 17:19:46 +05:30
Ziliang Peng
17cfa0f0a5 fix(background_review): inherit parent's reasoning_config to preserve Anthropic cache namespace
PR #17276 painstakingly pinned `_cached_system_prompt`, `session_start`,
`session_id`, and the toolset config on the background-review fork so its
outbound request body would byte-match the parent's and hit Anthropic's
exact-prefix cache. The contributor measured a ~26% end-to-end cost
reduction on Sonnet 4.5.

That optimization is currently being silently undone by a missing
`reasoning_config` kwarg. The fork's `AIAgent(...)` call omits it, so the
fork's `reasoning_config` defaults to `None`. `anthropic_adapter.build_anthropic_kwargs`
(line ~2165) then short-circuits the `thinking` / `output_config` block,
and the fork's request body lands in a DIFFERENT Anthropic cache namespace
from the parent's.

Result on the wire: 0 `cache_read_input_tokens`, full `cache_creation_input_tokens`
of the entire parent prefix — every single background review.

7 days of midagent.db traffic from one host running stock Hermes against
Anthropic Sonnet:

```
Background-review FIRST calls (the moment a review fork is born):
  count = 68
  cache_write tokens = 7,004,297
  cache_read tokens  = 1,016,335

Cost on Sonnet ($3.75/M write vs $0.30/M read):
  Spent on these writes:                      $26.27
  Cost if they had hit parent cache instead:   $2.10
  WASTED:                                     $24.16 / week / user
```

That is from one user. Multiply by Hermes's installed base for the full
impact.

Tested against api.anthropic.com directly (see refs/api-tests/ in the
attached investigation repo if needed):

| pair                                        | cache_r | cache_w |
|---------------------------------------------|---------|---------|
| parent fresh                                |       0 |  24,047 |
| parent same again                           |  24,047 |       0 |
| fork: appends 2 new tail msgs, thinking ON  |  24,047 |      22 |
| fork: appends 2 new tail msgs, thinking OFF |       0 |  24,047 |

Same fork-shape request, only difference is `thinking`. With the fix,
the fork hits the parent's full prefix and only writes the delta
(the `Review the conversation above…` prompt block, ~3-5K tokens).

One line in `agent/background_review.py`: pass
`reasoning_config=getattr(agent, "reasoning_config", None)` to the
`AIAgent(...)` constructor of the review fork. A short comment block
above it explains why so the next person who reads this code doesn't
re-introduce the regression.

`tests/run_agent/test_background_review_cache_parity.py` already covers
the system-prompt / session-id / toolset-config parity contracts that
PR #17276 introduced. I added:

* a `reasoning_config` attribute to `_make_agent_stub` so the stub has
  a non-None parent value the test can verify is propagated.
* `test_review_fork_inherits_parent_reasoning_config()` — asserts the
  fork's `AIAgent(...)` kwargs carry the parent's `reasoning_config`.
  Pre-fix this test fails with `None vs expected {'enabled': True, 'effort': 'medium'}`;
  post-fix all 4 tests in the file pass.

```
$ python -m pytest tests/run_agent/test_background_review_cache_parity.py -v
test_review_fork_inherits_parent_cached_system_prompt    PASSED
test_review_fork_pins_session_start_and_session_id       PASSED
test_review_fork_inherits_parent_toolset_config          PASSED
test_review_fork_inherits_parent_reasoning_config        PASSED  ← new
```

Also runs against the broader background-review test suite:
`test_background_review.py` (4), `test_background_review_summary.py` (8),
`test_background_review_toolset_restriction.py` (3) — 19/19 pass.

`agent/curator.py:1691` has the same omission for the umbrella-curation
fork, but curator's prompt is "curate all skills" — it shares no prefix
with any user conversation, so cache-parity is a non-issue there. Worth
auditing if the curator ever takes a parent conversation as input, but
not part of this PR.

The `agent/auxiliary_client.py:1006` `reasoning_config=None` hardcode is
intentional (title/summary one-shots on short prompts — per-call cost
of namespace flip is negligible) and is also out of scope.
2026-07-14 17:19:46 +05:30
HexLab98
55d826ccef fix(file-safety): distinguish safe-root write denial from credential blocks
Return actionable errors when HERMES_WRITE_SAFE_ROOT blocks a path instead of
labeling every denial as a protected credential file. Wire the helper through
write_file, patch, delete/move, and the Copilot ACP shim; sync docs examples.
2026-07-14 17:09:40 +05:30
webtecnica
78e844d446 fix(agent): validate credential pool after provider auto-detection (#63425)
Provider auto-detection (URL-based inference for Anthropic, OpenAI Codex,
and xAI endpoints) runs before credential-pool validation in AIAgent init,
but #63048 placed the pool validation before auto-detection. When the agent
is constructed with provider=None and a recognized endpoint URL, the pool
is validated against an empty provider identity and discarded, even though
auto-detection correctly resolves the provider moments later.

Fix: move the credential-pool validation block to after the URL-based
auto-detection chain. The pool is stored on the agent before
auto-detection; validation now checks the resolved provider and only
nullifies agent._credential_pool when the pool's scoped provider genuinely
doesn't match.

Regression test covers all three auto-detection paths:
- Anthropic (api.anthropic.com)
- OpenAI Codex (chatgpt.com/backend-api/codex)
- xAI (api.x.ai)

Fixes #63425.
2026-07-14 16:51:35 +05:30
kshitijk4poor
52cafa6f8e follow-up: integrate agent nudge + dispatcher retry docs and tests
- Nudge text now warns that repeated protocol violations will block the
  task and require manual intervention, so the model understands the
  consequence of ignoring the nudge.
- Kanban docs restructured to clearly separate the two defense layers:
  agent-side prevention (nudge, from #64350) and dispatcher-side
  recovery (bounded retry, from this PR).
- Two new integration tests verifying the nudge mentions blocking and
  that the agent-side and dispatcher-side budgets are independent.
2026-07-14 16:47:33 +05:30
kshitij
0684506072
Merge pull request #64004 from kshitijk4poor/salvage/63274-cli-close-persist
fix(cli): persist close transcript without history alias
2026-07-14 16:45:35 +05:30
kshitijk4poor
370ebf2d35 fix(skills): guard skill slash commands against core-command and slug collisions
scan_skill_commands() had two collision bugs in the same loop body:

1. Core-command collision: a skill whose normalized slug matches a core
   Hermes command name or alias (e.g. "skills", "learn", "bg") would
   get an auto-generated /command that shadows the core command in the
   gateway dispatch path (skill map is consulted before built-in
   handlers). The skill command silently overrode the core command.

2. Inter-skill slug collision: the seen_names set deduped on the raw
   frontmatter name, but the command map was keyed by the normalized
   slug. Two distinct names collapsing to the same slug (e.g.
   "git_helper" vs "git-helper") both passed the dedup, and the second
   silently clobbered the first.

Fix: add two guards in scan_skill_commands() after slug normalization:
  - resolve_command(cmd_name) check skips skills colliding with any core
    CommandDef (name or alias), logging a warning. Uses the existing
    resolve_command() API so aliases and case variants are covered
    without a separate cache. The skill remains loadable via /skill.
  - cmd_key in _skill_commands check dedups on the resolved slug,
    first-wins (preserving local-before-external precedence), logging
    a warning naming the shadowed skill.

Combines and supersedes #31204 (@cyrkstudios), #53450 (@Gridzilla),
#50304 (@petrichor-op), and #63305 (@Vissirexa).

Co-authored-by: cyrkstudios <cyrkstudios@users.noreply.github.com>
Co-authored-by: Gridzilla <Gridzilla@users.noreply.github.com>
Co-authored-by: petrichor-op <petrichor-op@users.noreply.github.com>
Co-authored-by: Vissirexa <Vissirexa@users.noreply.github.com>
2026-07-14 16:41:21 +05:30
mdc2122
03fbf6edbb fix(kanban): nudge workers that exit without complete/block
Add a bounded turn-end stop guard for kanban workers. When a worker
tries to exit with finish_reason=stop without having called
kanban_complete or kanban_block, inject up to two synthetic nudges
so the conversation loop continues instead of exiting cleanly (which
the dispatcher records as protocol_violation).

Mirrors the existing verify-on-stop pattern: same ephemeral scaffolding
flag (_kanban_stop_synthetic), same role-alternation contract, same
_pending_verification_response fallback for budget exhaustion.

Disabled by default (gated on HERMES_KANBAN_TASK env var set by the
dispatcher); kill switch via HERMES_KANBAN_STOP_NUDGE=0.

Salvaged from #62262 by @mdc2122. The original branch was 272 commits
behind main with ~538 files of stale-base reversions; this salvage
applies only the 4 substantive files (agent/kanban_stop.py,
conversation_loop.py insertion, run_agent.py _EPHEMERAL_SCAFFOLDING_FLAGS,
tests/agent/test_kanban_stop.py).
2026-07-14 16:34:00 +05:30
kshitijk4poor
3c2886f599 fix(conversation): clear _mute_post_response on substantive tool-only turn
Salvage of #63888. The original fix clears stale _last_content_with_tools
on substantive tool-only turns but doesn't clear _mute_post_response, which
a prior housekeeping turn may have set. This suppresses tool progress
output via _vprint until the no-tool-call branch resets it at line ~4834
— after all tools have finished executing.

Fix: also reset _mute_post_response = False when clearing stale fallback.

Added test: verify pure housekeeping turns (content + only housekeeping
tools) still set the fallback correctly — the original use case the
fallback was designed for.

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
2026-07-14 16:33:14 +05:30
liuhao1024
8a7d32d4e4 fix(conversation): clear stale housekeeping fallback on substantive tool-only turns
A cached _last_content_with_tools response from a housekeeping-only turn
could survive a later substantive tool-only turn. When the model returned
an empty response, Hermes incorrectly finalized the older housekeeping
narration instead of invoking the post-tool empty-response nudge.

Production impact: scheduled cron jobs could return early without completing
their actual work (e.g., daily report job returning a housekeeping message
instead of producing the report artifact).

Root cause: The fallback state was only updated when a turn had both
content AND tool_calls. A turn with tool_calls but empty visible content
would skip state updates entirely, leaving stale fallback state intact.

Fix: Classify tools in every tool-call turn (regardless of visible content).
When any tool is substantive (non-housekeeping), clear the older fallback state
before processing later empty responses. This prevents two-turn-old housekeeping
narration from being treated as if it belonged to the immediately preceding
substantive tool turn.

Regression test added: tests/run_agent/test_conversation_fallback_state.py

Fixes #63860
2026-07-14 16:33:14 +05:30
Teknium
89bd0fba90
feat(codex): redeem banked usage-limit resets via /usage reset (#64280)
OpenAI lets ChatGPT-plan Codex users bank rate-limit reset credits, but
until now they could only be redeemed from the Codex CLI/app or the
website. This wires the same backend API into Hermes:

- /usage on the openai-codex provider now shows "You have N resets
  banked - use /usage reset to activate" (parsed from the
  rate_limit_reset_credits field the /usage endpoint already returns).
- New /usage reset subcommand (CLI + gateway) redeems one banked
  credit via POST .../rate-limit-reset-credits/consume with a UUID
  idempotency key, mirroring codex-rs backend-client semantics
  (PathStyle /wham vs /api/codex, ChatGPT-Account-Id header,
  reset/nothing_to_reset/no_credit/already_redeemed outcomes).
- Guard: redemption is refused while no rate-limit window is fully
  exhausted, since a banked reset restores the FULL 5h + weekly
  allowance and spending it early wastes it. /usage reset --force
  overrides. Zero banked credits and non-codex providers are refused
  with clear messages; nothing_to_reset reports the credit was NOT
  spent.
- i18n: new gateway.usage.unknown_subcommand / reset_wrong_provider
  keys across all 16 locales; docs updated (cli.md, messaging index).

Tested with unit tests plus a real-socket E2E against a local fake
Codex backend exercising redeem/guard/force and the /usage hint.
2026-07-14 03:23:19 -07:00
Teknium
8582f35d96
fix(moa): flatten structured message content in the advisory view (#64319)
Cache-decorated turns (apply_anthropic_cache_control converts string
content to [{type: text, ..., cache_control}] lists — applied BEFORE the
MoA facade since the #57675 cache-cold fix) and multimodal turns
(text + image_url parts) flattened to empty strings in
_reference_messages, which only read str content. On turn 1 of a
provider:moa session with a Claude aggregator the references received a
single EMPTY user message: Anthropic-side providers 400'd ('messages: at
least one message is required') while tolerant models answered 'no user
request is present' (live incident Jul 14 2026, preset 'closed').

Fixes, in totality:
- _reference_messages: extract visible text via
  agent/message_content.flatten_message_text for user/assistant/tool
  turns (skips image parts, so no base64 leaks into the advisory view);
  decorated and undecorated transcripts now produce a byte-identical
  advisory view (advisor cache prefix stays stable).
- image-only user turns get a placeholder instead of an empty message
  (Anthropic rejects empty text blocks) or a silently dropped turn
  (would break user/assistant alternation).
- degenerate-case fallback flattens structured content too.
- _attach_reference_guidance: a decorated/multimodal trailing user turn
  now receives the guidance as a NEW text part appended AFTER the
  cache_control-marked part (cached prefix byte-stable) instead of
  falling through to a second consecutive user message (strict providers
  reject user/user).
- conversation_loop MoA injection: multimodal user turns get the MoA
  context appended as a trailing text part instead of being dropped;
  user_prompt for the one-shot path flattens content lists instead of
  str()-ing them (which leaked base64 payloads into the prompt).

Live-verified on the 'closed' preset (real OpenRouter wire, 2 user
turns, tool loop): all 4 reference calls carry the full document +
rendered tool state, end on user, zero tool-role/tool_calls; advisor
cache_write 7968 then cache_read 5909+; aggregator cache_read
14880-15237 on iterations 2+.

Co-authored-by: bo.fu <bo.fu@meituan.com>
2026-07-14 03:22:48 -07:00
Vishal Dharmadhikari
b8eb89f5c9 feat(gemini): improve request context for support and compatibility
Include the Hermes client name and version with Gemini inference, model and tier checks, and TTS requests. Add focused coverage for the request headers and keep the Gemini-specific context scoped to Google Gemini endpoints.
2026-07-13 22:26:19 -07:00
kshitijk4poor
2ccfdb2db4 fix(agent): exempt parseable vLLM/LM Studio output-cap errors from compression-disabled guard
Salvage of #63862. is_output_cap_error() returns False for vLLM/LM Studio
error messages that contain 'prompt contains ... input tokens' (treated as
input-overflow signal). But parse_available_output_tokens_from_error() CAN
extract a valid available_tokens from those same messages. The
compression-disabled guard only checked is_output_cap_error(), so vLLM/LM
Studio users with compression off still got a terminal failure instead of
the max-tokens retry.

Fix: also exempt when parse_available_output_tokens_from_error() returns a
value — that function determines whether the retry path can actually handle
the error, so it's the right predicate for the exemption.

Added test: verify vLLM-format error with compression_disabled=False still
triggers the max-tokens retry path.

Co-authored-by: dmabry <dmabry@users.noreply.github.com>
2026-07-14 03:58:54 +05:30
dmabry
127f6e1514 fix: exempt output-cap errors from compression-disabled guard 2026-07-14 03:58:54 +05:30
dmabry
57f1814832 fix: use provider available_out + request estimate for output-cap retry cap
The branch computed safe_out from estimate_messages_tokens_rough(messages),
but the provider rejected the larger api_messages request (system prompt,
injected context, tool schemas). When API-only content is large, safe_out
could far exceed the provider's available_tokens.

Compute safe_out from estimate_request_tokens_rough(api_messages, tools=...)
and keep provider available_out as an upper bound. Do not alter context_length
or trigger compression for output-cap errors.

Add production-path run_conversation tests that assert the retry API call's
max_tokens, including a case where a large system prompt makes messages-only
estimation undercount the real request.

Fixes #55546
2026-07-14 03:58:54 +05:30
dmabry
62ea800586 fix: recalculate safe_out from current input on each output-cap retry (#55546)
The retry loop computed safe_out from the error's available_tokens,
which reflected the *previous* request. Between retries the agent
appends tool results and error text, so the real input token count
grows. Deriving safe_out from the stale budget meant every retry
still exceeded the context ceiling by 1+ tokens, burning through the
3-attempt limit.

Compute safe_out from estimate_messages_tokens_rough(messages) so
the cap tracks the growing input on each retry attempt.
2026-07-14 03:58:54 +05:30
kshitijk4poor
658c011266 fix(deepinfra): restore provider-prefix aliases for model parsing
The _PROVIDER_PREFIXES frozenset in agent/model_metadata.py is static
and does not auto-extend from ProviderProfile. Removing deepinfra and
deep-infra from it broke provider:model prefix stripping for DeepInfra.
2026-07-14 03:34:25 +05:30
kshitijk4poor
8341d775a9 fix(session): restore clean API-local turn content 2026-07-14 03:32:46 +05:30
kshitijk4poor
0b422559f3 fix(session): preserve clean multimodal persistence override 2026-07-14 03:32:45 +05:30
kshitijk4poor
475922f2ce fix(cli): serialize close persistence handoff
Preserve one durable staged input across terminal close and the worker's early turn flush, without duplicating resumed transcripts or creating a session with a null prompt. Fixes #63766.
2026-07-14 03:32:45 +05:30
kshitijk4poor
2fc3f9c1ff fix(deepinfra): harden multimodal provider routing
Prevent credential forwarding across catalog redirects, retain explicit opt-in semantics for paid media backends, fail closed on invalid provider configuration, avoid mixed-catalog and output-limit assumptions, and reserve native STT provider names.
2026-07-14 02:59:39 +05:30
Georgi Atsev
fe002eb124 feat(providers): Support DeepInfra as an LLM provider 2026-07-14 02:59:39 +05:30
kshitijk4poor
af7dceaf77 fix(context): persist fallback compaction breaker 2026-07-14 02:19:40 +05:30
LeonSGP43
5ce827cac9 fix(context): count fallback compactions as ineffective 2026-07-14 02:19:40 +05:30
kshitijk4poor
2627933f33 fix(agent): distinguish missing from broken compression locks 2026-07-14 00:14:55 +05:30
Rory Ford
8f29c9f4e3 fix(agent): fail closed on unexpected compression-lock acquisition errors
Splits the single broad `except Exception` in the compression-lock
acquire path into two handlers: AttributeError/TypeError (version skew —
the lock method is missing, or predates the `ttl_seconds=` kwarg) still
fails OPEN as before, since that's known-safe to proceed without a lock.
Any other exception now fails CLOSED (skips compression this cycle)
instead of treating every failure as "no lock subsystem present" and
letting a second compressor run concurrently and fork the session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 00:14:55 +05:30
kshitijk4poor
0512f06a6a fix(auth): centralize pool auth normalization
Normalize Anthropic setup-token metadata for every PooledCredential construction path, persist corrected manual entries, heal legacy rows on load without copying global fallback credentials into profiles, and map the contributor email for release attribution.
2026-07-13 23:20:48 +05:30