Commit Graph

1893 Commits

Author SHA1 Message Date
Brooklyn Nicholson d39a031329 fix(nous): stop dropping "thinking off" on Portal models that can honor it
reasoning: {enabled: false} is the only shape the Portal honors, and the
profile refused to send it for every model. Sending nothing means the
upstream default instead, which on a thinking-first route like
deepseek/deepseek-v4-pro (catalog: default_effort high) is thinking ON — so
turning thinking off kept billing reasoning tokens on every turn.

The blanket omission was over-broad. The Portal only rejects a disable on
reasoning-mandatory routes ("Reasoning is mandatory for this model"), which
its catalog flags per model, so that flag now gates the omission. Models the
catalog can't speak to keep the old behavior rather than risk the 400.

extra_body.thinking, DeepSeek's own disable shape, is not forwarded upstream
by the Portal and is not an option here.
2026-08-19 22:14:56 -05:00
Jeff Mettel 22f66de638 fix(skills): publish and read the (map, platform) pair under one lock
Review feedback: publishing the map and its platform tag as two separate
global assignments is not atomic. A reader landing between them sees the
NEW map still carrying the OLD tag, and if that stale tag matches its own
platform it accepts the map without rescanning — serving another
platform's disabled-skill view, the leak #14536 closed.

Guard the pair with a module lock. scan_skill_commands publishes both
under it; get_skill_commands resolves its platform first, then reads the
map and tag together under the same lock to make the freshness decision.
Scanning stays outside the lock — it does file I/O and deferred imports,
and concurrent scans are already independent after the local-map change.

get_skill_commands now returns the scan's own completed map rather than
re-reading the global, so a concurrent publish cannot swap the result
between the decision and the return.

Adds a regression test that holds the publish lock and asserts a reader
cannot complete its lookup until it is released.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:41:42 -07:00
Teknium f7d90c9410 refactor: single canonical reasoning-effort vocabulary ends the per-vendor clamp drift
The #89503/#70058/#74295/#87279 bug class kept regenerating because every
transport and provider profile hand-rolled its own effort translation map
(9 sites, 4 distinct policies). New agent/reasoning_effort.py is the single
source of truth:

- EFFORT_LADDER: canonical low->high ordering (superset check against
  VALID_REASONING_EFFORTS pinned by test)
- clamp_effort(): one policy — supported passes verbatim, otherwise nearest
  WEAKER supported level (never escalate, never invert the ladder), floor
  when nothing weaker, 'none' never a degradation target, declared
  vendor-documented overrides win, bespoke names pass through
- declared wire vocabularies as data: OpenAI-compat, Codex Responses,
  xAI (4.6/legacy), Actual relays, Kimi K3/K2, TokenHub, GLM-5.2,
  DeepSeek V4, Ollama Cloud, Meta, Solar

Converted sites (all behavior-preserving except noted):
- chat_completions chokepoint, Kimi + TokenHub paths
- codex transport (backend branches now pick a declared set)
- auxiliary_client Responses path
- hermes_cli.models clamp_reasoning_effort_to_supported -> thin wrapper
- plugins: kimi-coding, zai, opencode-zen, deepseek, ollama-cloud,
  meta-ai, upstage, custom (copilot already routes via the wrapper)

Behavior fixes the shared policy surfaces:
- ollama-cloud/opencode-go 'minimal' now degrades to 'low' instead of
  being dropped (drop left the server default = MORE thinking than asked)

New tests: ladder contract (every configurable level is clamped by every
declared wire set; monotonicity across the full ladder for every set).
2026-08-19 19:29:10 -07:00
Jeffrey Quesnelle 612b3633d2 Merge pull request #77915 from bbednarski9/feat/relay-native-plugin-init
feat(relay)!: initialize static/dynamic plugins via native integration, remove opt-in plugin
2026-08-19 22:11:13 -04:00
Teknium 449471c334 feat: runtime stall guards — identical-call loop breaker and continue-intent recovery (agent.stall_guards)
Composio eval traces showed Hermes wasting turns re-issuing identical tool
calls (same tool, same args, same result — 3x/4x in one run) and ending
turns by announcing an action it never took. Two conservative, config-gated
guards (agent.stall_guards, default true):

- Identical-call loop breaker: ToolCallGuardrailController.observe_identical_call
  tracks the consecutive streak of (tool, canonical args, result-hash); on
  the 3rd identical call a compact one-line notice is appended to that tool
  RESULT at construction time (cache-safe — tool results are append-only).
  Never blocks the call. Pollers (process, *_get_result, *_poll) are exempt
  via STALL_GUARD_REPEATABLE_TOOLS. Streak resets on any different call,
  changed result, or new turn. Observed on the raw result before the
  tool-loop warning suffix so its changing count can't defeat matching.

- Said-continue-but-stopped recovery: trailing_continue_intent() detects a
  short reply ENDING on an announced next action ('Let me now…', 'I will
  now…', 'Next, I…'); the conversation loop feeds it into the EXISTING
  intent-ack continuation path (same interim-assistant + user-nudge
  mechanism, same codex_ack_continuations cap of 2), preserving message
  alternation — no parallel recovery machinery.

Config: agent.stall_guards in DEFAULT_CONFIG; docs in configuration.md;
unit tests for streak/allowlist/reset/gate and detector pos/neg cases.
2026-08-19 16:34:21 -07:00
Teknium 803397ecc3 feat: wall-clock run budget — wrap-up injection at 80% and deadline-scaled stale timeouts (agent.run_budget_seconds / --run-budget) 2026-08-19 16:32:17 -07:00
Teknium 09e657793e feat: MCP tool results spill at 50K and carry upstream-elision warnings
Composio-style MCP servers return un-paginated 22-47K-char payloads that
sail under the generic 100K per-result spillover threshold, bloating
context and ballooning per-turn reasoning time on long conversations.
Competitors cap harder (OpenCode/pi 50KB, Claude Code 30K, Codex ~10K
tokens). Three changes:

- mcp_* tools spill at a tighter 50K default (BudgetConfig.mcp_result_size,
  config-overridable via tool_budget.mcp_result_size_chars; pinned and
  per-tool overrides still win; capped by the context-scaled default).
- The persisted-output preview now teaches recovery: page the saved file
  with read_file or process with execute_code instead of re-requesting the
  same data from the remote API.
- Untrusted/MCP string results are scanned (bounded, first 64KB) for
  provider-side elision markers ('...N more items', "has_more": true,
  'saved to sandbox', data_preview) and get ONE cache-safe incompleteness
  notice appended at result-construction time, before untrusted wrapping —
  so the model stops treating provider-elided enumerations as complete.
- Hard 2M-char allocation cap in mcp_tool.py (text, error, and
  structuredContent paths) so a pathological multi-MB server payload is
  bounded before it propagates, while ordinary large results reach
  spillover intact. Distilled from #56060/#56072/#56511 (issue #56059);
  supersedes their 50K lossy truncation with spillover-friendly semantics.

Docs: configuration.md spillover-budget section + cli-config.yaml.example.

Co-authored-by: Stoltemberg <215755014+Stoltemberg@users.noreply.github.com>
Co-authored-by: AlexFucuson9 <295703459+AlexFucuson9@users.noreply.github.com>
Co-authored-by: Tranquil-Flow <66773372+Tranquil-Flow@users.noreply.github.com>
2026-08-19 16:31:16 -07:00
Teknium d762ed9b3c feat: execution-discipline guidance now reaches all tool-capable models (config model.execution_guidance)
Un-fences OPENAI_MODEL_EXECUTION_GUIDANCE from the gpt/codex/grok substring
check and gives it its own injection gate, independent of
tool_use_enforcement, controlled by config.yaml `agent.execution_guidance`
(auto/true/false/list — same semantics as tool_use_enforcement). The "auto"
list (EXECUTION_GUIDANCE_MODELS) now also covers deepseek, kimi, qwen, glm,
minimax, mimo, and mistral.

Composio agentic-eval traces showed Hermes+DeepSeek/Kimi failing where
competitors passed: financial math done in prose, no read-back after
external writes, malformed identifiers "repaired", completeness claimed
despite count mismatches. The discipline block existed but those models
never received it.

The block is extended with compact clauses distilled from that analysis:
- external-write read-back (tool-call success is not task success; internal
  file edits already confirmed by the tool are not re-verified)
- count reconciliation (declared totals/has_more are hard assertions)
- literal preservation (never normalize identifiers that fail a stated
  format; lookup success does not validate a malformed token)
- retry-differently (empty/partial/suspiciously narrow results get a
  broader retry before concluding)
- completion gated on verification (done = every named acceptance
  criterion verified, never a plausible subset)

The todo tool description now encourages enumeration-as-checklist for
"all N items" tasks and gates completed status on verified work, never
intent.

Guidance is chosen once at session start keyed on model name, so the
system prompt stays byte-stable for the life of a conversation.

Supersedes/absorbs prior contributor proposals: #20588, #35087, #41874
(MiMo), #53847 (GLM tool-calls-as-text stall).

Co-authored-by: Mat-London <56627804+Mat-London@users.noreply.github.com>
Co-authored-by: intelac <8803887+intelac@users.noreply.github.com>
Co-authored-by: 6ylqq <51219463+6ylqq@users.noreply.github.com>
Co-authored-by: tauros1983 <267660491+tauros1983@users.noreply.github.com>
2026-08-19 16:16:37 -07:00
Teknium 9ec5750aca fix: widen reasoning-effort wire translation to sibling sites (#89503 class)
The chat_completions chokepoint fix (ultra->max for every model,
cherry-picked from #89509) has siblings with the same bug shape:

- codex.py: ultra->max was gated on gpt-5.6 only; now baseline for all
  Responses-API models (backend-specific branches still override).
- Kimi top-level reasoning_effort: K3 accepts low/high/max only —
  'medium' and upper-ladder levels were dropped to the medium default
  (400s on K3, ladder inversion on K2). Full ladder mapped per family,
  mirroring the kimi-coding plugin's K3 map.
- TokenHub: 'minimal' fell through to the 'high' default (asked least,
  got most); full ladder now mapped onto low/medium/high.
- auxiliary_client Responses path: ultra->max alongside the existing
  minimal->low clamp.
- custom provider plugin: ultra capped at max instead of forwarded
  verbatim to GLM/vLLM/SGLang backends that reject it.
- copilot plugin: ad-hoc downgrade rules replaced with the shared
  clamp_reasoning_effort_to_supported ladder walk so ultra/max resolve
  to the strongest supported level instead of medium (#74295).

Sabotage-verified: new sibling-site tests fail 6/10 without the fixes.
2026-08-19 16:04:22 -07:00
liuhao1024 a664429192 fix(agent): cap the ultra reasoning level at the wire vocabulary
Hermes' internal effort vocabulary extends the wire set with ultra
(documented by /reasoning as none..xhigh|max|ultra). OpenAI-compatible
wires — OpenRouter chief among them — accept exactly
max|xhigh|high|medium|low|minimal|none and reject the extension with
HTTP 400, so an ultra configured while the default model was Anthropic
worked (the Anthropic adapter maps its own levels) but leaked
untranslated the moment a per-job override pinned a non-Anthropic
model, failing every call for that job.

The wire-compat chokepoint for this transport previously mapped
ultra to max only for gpt-5.6; generalize the cap to every model.
2026-08-19 16:04:22 -07:00
Falko 8430c1b4da test(codex): cover nested retention entry paths 2026-08-20 03:15:14 +05:30
Falko 8e2949495a fix(codex): strip unsupported cache retention at wire 2026-08-20 03:15:14 +05:30
Brooklyn Nicholson df9e2251f8 test(agent): give the MoA switch fake a reasoning_echo reader
663fa68cd4 added an unguarded `agent._read_reasoning_echo_from_config()` call
to switch_model's core field swap. The fake agent here carries only the
attributes switch_model touches, so the new call raises AttributeError inside
the rollback-protected block — every field the test asserts on gets restored
to its pre-swap value and all four cases fail on main with
`assert 'opencode-go' == 'moa'`.

Teach the fake the reader, matching the production AIAgent shape.
2026-08-19 13:56:11 -05:00
Yingliang Zhang 73243b0d2e feat(config): per-provider reasoning_echo opt-in for custom providers
Add model.reasoning_echo (default false) and per-fallback-entry
reasoning_echo to preserve assistant reasoning_content when
replaying history to custom providers and OpenAI-compatible gateways
that proxy thinking-mode models (Kimi K3, GLM-5.2, DeepSeek, etc.)
but are not matched by the built-in host-based _REASONING_ECHO_RULES.

The flag is per-active-provider, not a global toggle:
- Primary: read from model.reasoning_echo at init and switch_model
- Fallback: set by try_activate_fallback from the fallback entry
- Restore: restore_primary_runtime copies the switch_model snapshot

Unlike PR #76019 global agent.reasoning_echo toggle, the
per-provider flag travels with the active provider — falling back to
a strict provider (Mistral, Groq, Cerebras) correctly strips
reasoning_content even when the primary had the flag enabled,
because the flag is False for the strict fallback.

Complements PR #27361 (dynamic detection) which fires after the first
API response; this PR covers turn-1 and history-replay-on-fresh-session
where dynamic detection has not fired yet.

Closes #76018
Refs: #27297, #27361, #76019

Signed-off-by: Yingliang Zhang <zhangyingliang@outlook.com>
2026-08-20 00:03:50 +05:30
Bryan Bednarski bc92c875ef test(relay): defer compaction flush expectation
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-19 09:14:12 -07:00
Alex Fournier 31402f630b fix(relay): complete native plugin cutover
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-08-19 08:52:03 -07:00
Bryan Bednarski 93bec27f66 fix(relay): preserve shutdown scope cleanup
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-19 08:52:03 -07:00
Bryan Bednarski 6ec2c0ba8b fix(relay): define process-wide profile policy
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-19 08:52:03 -07:00
Bryan Bednarski 3fad83df31 fix(relay): guard native plugin ownership cutover
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-19 08:52:03 -07:00
Bryan Bednarski ad9fb060c5 fix(relay): clarify opt-in plugin layering
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-19 08:52:02 -07:00
Bryan Bednarski c86fd74e28 refactor(relay): require 0.7 plugin APIs
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-19 08:52:02 -07:00
Bryan Bednarski 4e34dfc932 refactor(relay): use canonical dynamic plugin config
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-19 08:52:02 -07:00
Bryan Bednarski e8644e05a3 refactor(relay): remove legacy observability plugin
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-19 08:52:01 -07:00
Bryan Bednarski 0b7288ebcb fix(relay): require explicit plugin configuration
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-19 08:52:01 -07:00
Bryan Bednarski e7da915f67 fix(relay): defer subscriber flush to shutdown
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-19 08:52:01 -07:00
Bryan Bednarski 918dd8a265 feat(relay): load standard dynamic plugin records
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-19 08:52:01 -07:00
Bryan Bednarski 88300217c2 feat(relay): activate configured dynamic plugins
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-19 08:52:01 -07:00
Bryan Bednarski c4ae7f7a3b feat(relay): initialize discovered plugin components
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-19 08:52:01 -07:00
ethernet 3c675019f1 fix(aux): retry once without response_format when a provider rejects it
Some providers reject the structured-output request field with a hard
400. The error classifier marks a 400 as non-retryable, so one rejected
field failed the whole auxiliary call. Session titles stayed derived
forever (#82816), and no fallback fired.

Three rejection shapes are covered, from live reports:
- vLLM gateways translate response_format into guided_grammar and fail
  when the grammar backend is absent (compile_grammar_error: No module
  named 'xgrammar').
- Some OpenAI-compatible endpoints answer "This response_format type
  is unavailable now".
- Anthropic-compatible gateways that predate structured outputs reject
  the translated field: "output_config: Extra inputs are not
  permitted". The documented case is the bedrock-mantle Messages
  endpoint.

The fix is reactive, the same pattern as the temperature and
max_tokens rungs: when the provider rejects the field, retry once
without it. Callers tolerate an unconstrained reply — the title prompt
demands bare JSON and _extract_title_text has a loose-JSON fallback —
so the call succeeds with prompt compliance instead of failing. The
retry only fires when the request carried the field, and both the sync
and async paths get the same rung.

Closes #82816
2026-08-18 20:34:46 -04:00
ethernet 8f2d61e3de fix(aux): translate top-level response_format kwarg on the Anthropic adapter
The adapter builds the Messages body from a fixed allow-list of kwargs.
A caller that passes response_format as a top-level kwarg (the OpenAI
SDK call shape) got it dropped on the floor. The request succeeded, but
the schema contract silently became prompt compliance. No in-tree
caller uses this shape today. The pin-test makes sure that a future
refactor cannot open this leak again.

The top-level kwarg gets the same output_config.format translation as
the extra_body shape. When a caller sends both shapes, the extra_body
value wins because every in-tree caller uses that shape.

Pin-test pattern from PR #85626 review follow-up.

Co-authored-by: Matt McClean <mmcclean@amazon.com>
2026-08-18 20:34:46 -04:00
Soju06 f709bd8445 fix(aux): translate response_format to output_config.format for anthropic transport
Plugin structured completions (plugin_llm.complete_structured) build an
OpenAI Chat Completions response_format payload in extra_body. The
anthropic_messages transport forwarded it verbatim, and strict
Anthropic-compatible gateways reject it with HTTP 400:

  response_format: OpenAI Chat Completions structured-output shape is
  not supported. Use output_config.format = {"type": "json_schema", ...}

Observed live: every discord-thread-autotitle structured call failed
for 2+ days (1,600+ logged errors) once the main provider became an
anthropic_messages gateway.

Fix: _translate_anthropic_response_format converts
- json_schema  -> output_config.format = {type: json_schema, schema: S}
- json_object  -> permissive object schema (SDK 0.87.0 has no
  schema-less JSON mode)

merging into any existing output_config (adaptive-thinking effort
coexists) and excluding response_format from the raw extra_body
passthrough alongside the existing reasoning exclusion. The async
adapter delegates to the sync adapter via asyncio.to_thread and is
covered by a test. Non-Anthropic transports are unchanged.
2026-08-18 20:34:46 -04:00
Jeffrey Quesnelle 9664e386f6 Merge pull request #85581 from bbednarski9/codex/fix-openai-sparse-response-objects
fix(openai): tolerate sparse response objects
2026-08-18 11:42:24 -04:00
Jeffrey Quesnelle aa09c8fe8c Merge pull request #85580 from bbednarski9/codex/fix-relay-client-timeout-payload
fix(relay): keep client timeout off managed payloads
2026-08-18 11:33:51 -04:00
Teknium 7a0cdbcd79 fix(tests): pin workspace snapshot in plugin-prompt-sections byte-stability test
test_real_aiagent_builds_section_once_and_keeps_it_out_of_static_prefix
builds the system prompt twice and asserts byte equality, but the prompt
embeds build_coding_workspace_block() — live git status/log output. A git
call failing between the two builds (xdist contention in CI) makes the
Branch/Recent-commits lines differ and fails the test on unrelated PRs
(first seen on #89027's run: diff showed only '- Branch: (detached HEAD)'
and recent-commit lines).

Mechanism reproduced locally: with coding posture on and cwd inside the
checkout, failing git calls on the second build only => first != rebuilt;
with the snapshot pinned, identical git failure => byte-equal. The real
block's byte-stability is coding_context's own contract; this test is
about plugin sections.
2026-08-18 02:12:30 -07:00
Bryan Bednarski a51337ecee Merge main into fix-openai-sparse-response-objects
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-17 18:58:48 -07:00
Bryan Bednarski 0f5f5a5996 Merge main into fix-relay-client-timeout-payload
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-17 18:55:18 -07:00
Jeffrey Quesnelle e818025b4d Merge pull request #85582 from bbednarski9/codex/fix-relay-lazy-completed-streams
fix(relay): unwrap lazy completed streams
2026-08-17 21:39:52 -04:00
Jeffrey Quesnelle 75bbc055a7 Merge pull request #85579 from bbednarski9/codex/fix-relay-canonical-operation-names
fix(relay): use canonical managed operation names
2026-08-17 21:29:18 -04:00
Teknium 6e22d26583 feat: project-skill quarantine + non-interactive trust inheritance
Completes the project-local skills epic's remaining skill items (#48974,
#48975) on top of the discovery/trust work in #88566.

Quarantine (#48974): trust is a repo-level decision made once, but repo
skill content changes with every pull — the hub install path scans, a
checkout didn't. Every project SKILL.md dir now runs through the same
skills_guard scanner as hub installs (content-hash cached under
~/.hermes/cache/project_skill_scans/, never inside the repo). Verdict
'dangerous' quarantines the skill: excluded from the index, skills_list,
and slash commands via the single iteration chokepoint
iter_project_skill_files(), and skill_view refuses by name with an
explanatory error. Scanner failure fails closed. Verified against a real
injection fixture (6 findings: prompt_injection_ignore, deception_hide,
invisible_unicode, credential exfil patterns).

Non-interactive inheritance (#48975): find_project_root() now resolves
from TERMINAL_CWD (the per-surface workdir cron jobs and the terminal
tool already use) before falling back to process cwd. Cron/API/ACP
surfaces inherit a prior interactive trust decision by project identity:
job workdir inside a trusted repo => project skills load; untrusted or
no workdir => nothing loads; no surface ever prompts.

Tests: +10 cases in tests/agent/test_project_skills.py (real malicious
fixture, fail-closed, rescan-on-change, cache location, TERMINAL_CWD
inheritance matrix). Docs: quarantine + non-interactive sections in
skills.md.
2026-08-17 14:06:16 -07:00
Lavie 9cf553ca3e refactor(agent): document meta api_mode fallback intent + review cleanups
Implement Claude Opus review findings for Meta API support:
- Document in agent/agent_init.py that provider="meta" without an api.meta.ai URL falls through to chat_completions by design (URL-driven wire selection).
- Comment on suppression guard in hermes_cli/runtime_provider.py noting api.meta.ai is handled by _detect_api_mode_for_url.
- Replace inline __import__ with top-of-module import in tests/hermes_cli/test_model_switch_openai_api_mode.py.
- Rename test_meta_retention_not_sent_when_overridden -> test_meta_retention_override_wins in tests/agent/transports/test_meta_codex_cache.py.
- Add test in tests/agent/test_meta_agent_init.py for provider="meta" fallback without api.meta.ai URL.
- Add test in tests/agent/test_auxiliary_client.py for prompt_cache_retention: "24h" under _CodexCompletionsAdapter.

Source: Claude Opus review findings for feat/meta-api-support.
2026-08-17 12:58:51 -07:00
Lavie d4658ee6d5 fix(agent): preserve provider-slug rewrite when host mandate fires
Relocate host_mandated_api_mode check from top of api_mode cascade to
fallback else branch so URL-based provider-slug rewrites (e.g.
api.anthropic.com -> provider='anthropic') always run first. Previously
the mandate branch set api_mode for api.anthropic.com without rewriting
provider, leaving provider='' and causing credential_pool_matches_provider
to fail closed and discard anthropic-scoped pools (#63425 regression
introduced in 8f60e8263).

The mandate is now a true fallback for hosts without an elif branch
(api.meta.ai -> codex_responses for 93-99% prompt-cache hits vs 0% on
chat, plus future mandates) with lazy import + try/except preserved.

Add regression tests: provider=None + api.anthropic.com URL implies
provider='anthropic'/api_mode='anthropic_messages' and preserves an
anthropic credential pool; provider=None + api.meta.ai URL implies
codex_responses.
2026-08-17 12:58:51 -07:00
Lavie 24545418eb fix(providers): route api.meta.ai through Responses API for prompt caching
- hermes_cli/providers.host_mandated_api_mode: add exact-hostname clause for
  api.meta.ai → codex_responses (measured 0% cache on /chat/completions vs
  93-99% on /responses with retention); update docstring.
- hermes_cli/runtime_provider._detect_api_mode_for_url: mirror clause for
  api.meta.ai (exact hostname, #32243) to keep runtime resolver in lockstep.
- agent/agent_init: call host_mandated_api_mode early in api_mode cascade
  (after explicit api_mode wins, before provider-name specials) via lazy
  import; single source of truth, preserves user override.
- agent/transports/codex._default_prompt_cache_retention_for_request: return
  24h for api.meta.ai unconditionally; build_kwargs setdefault preserves
  override; Bedrock branch untouched.
- cli-config.yaml.example: add commented providers.meta example (api_mode
  auto-detected).
- website/docs/developer-guide/adding-providers.md: list Meta alongside
  Codex/xAI as codex_responses native provider with retention note.
- tests: add hermetic behavior-contract suites for mandate, retention,
  content-addressed prompt_cache_key, reasoning passthrough, AIAgent init,
  usage cache reporting, model-switch override, and config roundtrip; extend
  test_model_switch_openai_api_mode with meta cases.
2026-08-17 12:58:51 -07:00
Teknium f891d702df feat: project-local skill discovery with per-repo trust gate
Sessions started inside a git checkout now source skills from
<root>/.hermes/skills/ and <root>/.agents/skills/ (the cross-tool
convention shared with other agent harnesses) as the highest-precedence
skill tier: project > local > external_dirs.

Loading is trust-gated per repo (skills.trusted_project_dirs, managed by
'hermes skills trust'/'untrust') because skills are executable procedure
documents — auto-sourcing them from any cloned repo is a prompt-injection
vector. Untrusted repos with skills get a one-line banner notice instead.

- agent/skill_utils.py: find_project_root, get_project_skills_dirs,
  get_untrusted_project_skills_root, get_scan_ordered_skills_dirs;
  project dirs join the curator read-only ownership boundary
- agent/prompt_builder.py: project tier scanned first, entries tagged
  [project], same-named local entries shadowed; cache key extended
- tools/skills_tool.py: skills_list scans project dirs first (first-wins);
  skill_view resolves cross-tier collisions in favor of the project tier
  (same-tier ambiguity still refuses); security warning recognizes the tier
- agent/skill_commands.py + hermes_cli/commands.py: /skill-name slash
  commands and gateway slash menus include project skills
- tools/credential_files.py: project dirs mounted into remote backends
- cli.py: banner notice (loaded count / trust hint)
- hermes_cli/main.py + subcommands/skills.py: hermes skills trust/untrust
- config: skills.project_discovery (default on), skills.trusted_project_dirs
- docs: Project-Local Skills section in skills.md
- tests: tests/agent/test_project_skills.py (18 cases)

Session cwd is fixed at agent build time, so the resolved tier is stable
for the conversation and the system prompt stays byte-stable (cache-safe).
2026-08-17 11:39:13 -07:00
Alex Fournier 7ee68cca45 test(relay): cover lazy completion with interceptor
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-08-17 10:21:32 -07:00
Alex Fournier cc3418e069 fix(openai): complete sparse response normalization
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-08-17 10:17:52 -07:00
Alex Fournier 256d7efbfd test(relay): document stream priming boundary
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-08-17 09:39:11 -07:00
Jack Lau cf64ca20c5 fix(compression): stop an aborted rotation from growing the parent it could not publish
The rotation path flushes its un-persisted transcript to the parent (#47202)
and only then calls publish_compression_child. The abort handler rolls back
the in-memory transcript and keeps agent.session_id on the parent - its own
comment says "keep the parent live and discard the stale compacted snapshot" -
but the rows the flush just wrote are not part of what it discards. Every
failed rotation therefore leaves the parent transcript longer than it found
it, whatever the failure was.

That is survivable for a one-off failure and pathological for a sticky one.
A parent row carrying ended_at fails the publish on every attempt and nothing
in this path clears it, so each auto-compaction appends another copy of the
current turn to the transcript it was supposed to shrink. Worse, the growth
then satisfies conversation_compression's own len(durable_parent) >
len(messages) check, so the next attempt adopts the inflated snapshot as if it
were genuine concurrent activity and the in-memory transcript doubles too.

Check that one precondition before writing. It is a plain read of the row the
publish is about to read anyway, and it raises the publish's own message, so
split_status=aborted, failure_class=session_split_failed and the rollback path
are all unchanged; a live parent reaches the flush exactly as before.
Deliberately not extended to the compression lease, which is re-acquirable - a
transient miss there would abort a rotation that would otherwise have
committed. old_session_id moves above the flush so a failure raised from here
takes the same in-memory rollback as any other pre-publish failure.

Scope: this fixes the amplification for every abort cause. It does not fix
what marks a live session as ended in the first place (#88197 Bug 1), which
needs a maintainer decision on end-reason taxonomy and is tracked on the
issue; an affected session still aborts every attempt, it just stops making
itself larger while it does.

Refs #88197
2026-08-17 18:15:46 +05:30
Teknium d5167831b8 Port from can1357/oh-my-pi#7306: reject answer-shaped auto-title output
A tiny title model that ignores the 3-7 word titling task and answers
the user's first message instead used to have its whole reply stored
(truncated at 80 chars) as the session title. Truncating an assistant
blob still leaves an assistant blob — generate_title now rejects output
over 12 words and returns None, letting maybe_auto_title retry on the
next exchange. The 80-char truncation remains for genuine-but-wordy
titles that pass the word bound.
2026-08-16 22:10:02 -07:00
Teknium 4be4b9866e Port from earendil-works/pi#7494: preserve Gemini 3 tool call IDs
Gemini 3+ models require explicit tool call IDs on functionCall /
functionResponse parts in replayed history; without them parallel tool
calls can be rejected or mispaired. The native adapter now:
- threads the model id into request building and includes ids for
  Gemini >= 3 (version-gated: 2.x rejects unexpected id fields)
- preserves provider-returned functionCall.id on both non-streaming
  and streaming responses instead of always minting a random one
2026-08-16 22:07:52 -07:00
Teknium a8d5e16ccf Port from earendil-works/pi#7681: support AGENTS.override.md context override
AGENTS.override.md now takes priority over AGENTS.md in both startup
project-context loading (prompt_builder) and progressive subdirectory
hint discovery (subdirectory_hints). Lets developers keep a personal,
typically-gitignored override next to committed project instructions
without editing the tracked file.
2026-08-16 22:07:43 -07:00