Commit Graph

2832 Commits

Author SHA1 Message Date
Teknium 03e9f4caee Merge pull request #104284 from NousResearch/fix/nous-anthropic-chat-wire
fix(nous): route anthropic/* over chat/completions by default; the native Portal route re-writes the cache on 14-20% of calls (nous.anthropic_wire)
2026-09-06 07:17:48 -07:00
Teknium b1403d521c Merge pull request #104168 from NousResearch/fix/subagent-cache-ttl-5m
fix(delegate): subagents never inherit the 1h prompt-cache tier (2x write price for retention they never use)
2026-09-06 07:17:43 -07:00
Teknium 028fe2c4c8 feat(delegation): per-task completion groups — ungrouped subagents return as they finish
A background delegate_task call used to be ONE async unit: the runner joined on
every child and a single consolidated message re-entered the conversation when
the SLOWEST finished. Fifteen independent PR reviews therefore waited on the
fifteenth before the parent could act on the first.

Each task now carries an optional `group`. `_units_of` partitions the call's
children into units — one per distinct group, one per ungrouped task — and each
unit is dispatched to the async registry on its own, so its results re-enter the
conversation as soon as THAT unit is done. Tasks that must be compared or merged
share a group and still return together.

Capacity is unchanged: every unit of one call joins the first unit's pool slot
(`slot_key` in `async_delegation._dispatch`), so splitting never consumes more of
`delegation.max_concurrent_children` than the call did. Unit ids suffix the
call's id (`deleg_xxxx-1`, `-2`, …) so live transcripts stay under one dir; the
completion block names the group and notes that sibling units report separately;
`active_task_count` counts a unit's own tasks.
2026-09-06 07:17:30 -07:00
Xipong 25a80b3bb6 fix(skills): exclude generated runtime caches from bundled ownership hashes 2026-09-06 07:17:03 -07:00
Teknium 335ecf9f4a test: the two remaining native-wire contracts select nous.anthropic_wire=native explicitly
test_nous_anthropic_fallback_uses_the_messages_wire and
test_nous_child_rederives_api_mode_from_model describe the native wire, which
is now opt-in; select it in the test the same way the wire-contract suite
does, so both keep guarding the flip-back.
2026-09-06 05:55:18 -07:00
Teknium 3a15c39e8e fix(banner): deferred update notice keeps prompt_toolkit routing; drop dead console arg; PTY harness
Follow-up on the cherry-picked fix: _defer_update_notice no longer takes the Rich console it
can never safely use (the notice always lands after patch_stdout owns stdout), the unit test
asserts the contract at prompt_toolkit's boundary (an ANSI fragment reaches
print_formatted_text, with no raw ESC/markup in the visible text) instead of patching our own
cprint, and evals/cli_deferred_notice.py drives the real CLI under a Linux PTY with a FIFO-gated
update cache to A/B the late-notice cases (garbled on base, clean after).
2026-09-06 05:36:26 -07:00
turingcat a366a0bb17 fix: route deferred update notice through cprint to avoid ANSI garbling
The deferred update notice ("N commits behind") was rendered via
console.print which writes ANSI escapes directly to stdout. Under
patch_stdout (active in the interactive CLI), the StdoutProxy strips
ESC bytes, leaving visible [1;33m...[0m artifacts.

Fix: render Rich markup to an ANSI string via a captured Console, then
route through cprint (which uses prompt_toolkit ANSI parser) to
bypass the StdoutProxy mangling.

This is the same class of bug as #2262 (fixed by #2448 for agent._print_fn
and display.py), but the _defer_update_notice callsite in banner.py was
not covered by that fix.

Fixes #83969
2026-09-06 05:36:26 -07:00
Teknium 3009efe75e fix(tools): read_file dispatch honors the advertised 2000-line default when the model omits limit
#76996 raised DEFAULT_READ_LIMIT, the read_file_tool signature and the schema
default from 500 to 2000, but the registry dispatch handler `_handle_read_file`
kept its own literal `args.get("limit", 500)`. Since models omit `limit` on most
reads, every dispatched read still stopped at 500 lines with `truncated: true`,
so the default flip never reached production traffic.

All three sites now read the single DEFAULT_READ_LIMIT constant, so the schema,
the Python default and the dispatch fallback cannot drift again.

Live probe (registry.dispatch on a 1500-line file, no limit): before 501 lines /
truncated=true; after 1501 lines / truncated=false.
2026-09-06 05:32:49 -07:00
Teknium 0cb996d977 fix(delegation): number delegation batches per conversation, not per process
`format_batch_tag` handed out `set N` ordinals from one process-wide table,
so every conversation on a shared backend and every child's nested fan-out
advanced the same counter. A user's second wave of 15 lanes rendered as
`[set 20 · 13/15]`, which reads like 20 batches were spawned.

Scope the ordinal table by the parent conversation (`parent_agent.session_id`)
and thread the parent through the three render sites (batch header,
completion lines, child tree-line prefix via the shared session_ref). The
first fan-out in a conversation is `set 1`, the next `set 2`; sibling
conversations and nested child batches no longer inflate it.

Invariant test proven red on origin/main, green with the fix.
2026-09-06 05:03:20 -07:00
kshitijk4poor 3513a3b922 test(process_registry): pin that the availability probe argv omits OOMPolicy too
Requested in the review of #102357: the spawn builder was pinned, the probe
was not, and the probe is where the rejection was cached as "unavailable".
2026-09-06 14:57:33 +05:30
kshitijk4poor 04885c25cf test(process_registry): pin that scope argv never emits OOMPolicy
The spawn_local systemd test asserted the property was present; it now
asserts the invariant the fix establishes (no OOMPolicy= on a transient
scope), so a reintroduction fails here instead of on older systemd hosts.
2026-09-06 14:57:33 +05:30
kshitijk4poor 10e8756cf4 test(memory): drop assertions implied by the disk-unchanged check 2026-09-06 14:53:13 +05:30
Halldrix 7602b33d79 fix(memory): point empty-batch refusal at single remove() calls (#103419)
Review feedback: the sanctioned deliberate wipe is repeated single-op
remove() calls, not a manual file edit — direct the model there.
2026-09-06 14:53:13 +05:30
Halldrix faa46ed2c7 test(memory): pin batch refusal to empty a non-empty store (#103419) 2026-09-06 14:53:13 +05:30
Teknium 0edb6b928a fix(delegate): subagents never inherit the 1h prompt-cache tier
A delegated child copies the parent's prompt_caching.cache_ttl. The 1h tier
is priced for a person who steps away between turns (2x write vs 1.25x for
5m); a subagent calls every few seconds for minutes and is gone, so it paid
2x on every tool result and never collected the retention. Live measurement
(Sep 5, 40 concurrent Fable 5.1 children via OpenRouter, 1h markers on the
wire): cache writes billed at $20/M against $12.51/M for the same run at
5m, ~60% of a write-dominated bill.

_apply_child_cache_ttl runs right after child construction: 1h -> 5m,
5m stays, disabled stays disabled, parent untouched. Tests: unit (markers
on the wire drop ttl; parent's 1h layout still differs) and the real spawn
path with cache_ttl: 1h in a temp HERMES_HOME.
2026-09-06 02:20:49 -07:00
kshitijk4poor 9a84bee265 refactor(ssh): drop probe-only bookkeeping the probe never reads
- probe_only is consumed inside __init__ only; no instance attribute.
- _sync_manager is assigned only on the probe branch, after the connection is
  up, so a normal SSHEnvironment whose constructor fails still behaves exactly
  as before (its __del__ cleanup does not reach the shared socket teardown).
- _remote_home has no reader on the probe path; not assigned.
- prompt_builder passes probe_only=True unconditionally: which backends honor
  it is the builder table's decision, not a caller-side env_type check.
- Tests trimmed to the probe_only contract: the cleanup-raises case was green
  on main (pre-existing try/except), the socket-name length and double-cleanup
  assertions covered pre-existing behaviour.
2026-09-06 13:32:51 +05:30
Ruichen Zhou 50b54bb18f fix(ssh): isolate prompt backend probes from file sync
The prompt-time backend probe built a normal SSHEnvironment just to run a
one-line `uname`. That constructor detects the remote home, creates the
~/.hermes tree, force-uploads every sync file and snapshots a login session;
when the throwaway object was later garbage-collected, __del__ -> cleanup()
ran sync_back() and `ssh -O exit` against the ControlMaster socket the
agent's real environment shares (keyed by user@host:port).

Add an internal probe_only construction path for SSH: an isolated,
same-length ControlMaster socket (keyed by the instance's session id), no
remote dir setup, no FileSyncManager, no session snapshot. The probe now
tears its own connection down explicitly on success, non-zero exit and
exception, without replacing the probe result when cleanup fails. Normal
SSH callers and non-SSH backends are unchanged.

Salvaged from #77933 onto the facade/sibling layout (the probe body moved to
_run_backend_probe, _create_environment to tools/terminal_tool_backends.py).
2026-09-06 13:32:51 +05:30
Muno459 3f88055035 test(mcp): integration regression for colliding static toolset name via _register_server_tools 2026-09-05 17:00:18 -07:00
Teknium 463292351f fix: mid-turn user message no longer waits behind a foreground terminal command
A message typed while the agent runs (CLI busy_input_mode=interrupt, gateway
priority redirect, ACP redirect) goes through AIAgent.redirect(). During tool
execution redirect() degrades to steer(), whose delivery rides the tool result
— so a long foreground command (a `sleep 285` CI poller, a build) parked the
user's message until it exited. The UI printed "Redirected current turn" while
nothing happened for minutes.

redirect() now also asks the tool workers to YIELD (tools/interrupt.request_yield).
The local terminal backend's wait loop honours it: the drain thread is stopped, the
still-running Popen is adopted by the process registry as a notify_on_complete
background session (ProcessRegistry.adopt_local — output so far seeds the buffer,
the registry reader continues from the pipe), and the tool returns immediately with
status "yielded_to_background" + session_id. The command is never killed; the
completion notification arrives as usual and process(poll/wait/log/kill) work on it.

Non-local backends and internal env.execute() consumers pass no yield_handler and
are unaffected; a stale yield bit is cleared with the interrupt bit per worker tid.
2026-09-05 13:51:06 -07:00
kshitijk4poor 9dd6634c56 test(docker): atexit drain joins the worker of an env already popped from the registry; finished workers do not accumulate
Both red on origin/main.
2026-09-05 21:07:14 +05:30
kshitijk4poor b800fdf9a8 test(docker): snap_compat drops only --init and no-new-privileges; key bridged on every config path 2026-09-05 21:00:19 +05:30
kshitijk4poor 431978d47b test(file_sync): backend can reopen the staging tar for writing; POSIX remote keys match on a Windows host (windows_only) 2026-09-05 20:49:29 +05:30
kshitijk4poor 006b1beb00 test(tools): dead environment surfaces as unavailable across read_file/read_file_raw/read_file_bytes/search; healthy env still reports missing
Trimmed from the contributor's suite to two parametrized contracts. Red on origin/main (6 cases).
2026-09-05 19:06:57 +05:30
kshitijk4poor bb504a2e14 fix(terminal): every Windows drive letter is a host cwd, not just C: (#60962)
_HOST_CWD_PREFIXES only listed C:\ and C:/, so a D:\ or E:/ working directory on a
Windows host reached docker run -w and the container started in a directory that does
not exist. The two prefix scans now go through one _is_host_cwd predicate that matches
any drive letter with either slash.
2026-09-05 19:01:22 +05:30
kshitijk4poor 7b8cf4c631 test(code-execution): a changed tool set spawns a fresh remote kernel with fresh stubs
Red on origin/main.
2026-09-05 18:46:53 +05:30
kshitijk4poor ef89a7f1c9 test(tools): every truncation footer renders the backend-visible cache path under docker; local keeps the host path
Red on origin/main.
2026-09-05 18:46:16 +05:30
kshitijk4poor deaebd9b17 test(tools): probe and execute_code hand _create_environment the same container_config as the terminal tool
Replaces the source-reading AST check with a behaviour contract. Red on origin/main.
2026-09-05 18:32:44 +05:30
kshitijk4poor d72cbbcf5a test(docker): lockdown + operator --network none emits one flag; contradictory network fails closed
Trimmed from the contributor's five tests to the two contracts. Red on origin/main.
2026-09-05 18:19:58 +05:30
kshitijk4poor 9e23028e2e test(approval): deny rule blocks in every isolated backend under yolo/off; non-denied commands still skip
Red on origin/main (10 parametrized cases).
2026-09-05 18:08:00 +05:30
kshitijk4poor c957432685 test(security): corrupt-token and docker_env warnings never echo the value
Red on origin/main.
2026-09-05 18:05:15 +05:30
kshitijk4poor bde6f6dce2 test(docker): reuse probe never emits the Docker-only {{.Label}} template; egress-off posture is a label filter
Red on origin/main.
2026-09-05 18:04:52 +05:30
rainbowgits 179e988493 fix(terminal): JSON-serialize list/dict values in profile terminal scope 2026-09-05 18:04:00 +05:30
kshitijk4poor 5035fbde1c test(tools): plugin container backend gets the host-cwd guard on the file-tools path; non-container plugin keeps it
Red on origin/main, green with the fix.
2026-09-05 18:03:05 +05:30
RelaxJonh c851ef4d0f fix(tools): file-tools env creation uses _is_container_backend() for the cwd guard (#101013)
_create_terminal_env_for_file_ops tested the static _CONTAINER_BACKENDS set while the
terminal tool's sibling sites already go through _is_container_backend(), which also
honours plugin backends declaring is_container. A host cwd override on such a plugin
backend therefore reached the sandbox as its workdir from the file-tools path only.

Salvaged from #101389 by @RelaxJonh.
2026-09-05 18:03:05 +05:30
kshitijk4poor baf7ceeace refactor(environments): share the unset-prelude and client-env helpers; drop test-only shape from ssh
prepend_unset / client_env_with live beside resolve_passthrough_env so docker and ssh
post-process its output identically. The ssh tests build a real SSHEnvironment under
the existing connection stub instead of a half-initialised __new__ object, so the
production code no longer carries getattr fallbacks for it.
2026-09-05 15:54:39 +05:30
kshitijk4poor be6f0d296b test(ssh): passthrough goes out via SendEnv, values never in remote argv, provider keys held back
Red on origin/main (no SendEnv, no client env), green with the fix.
2026-09-05 15:54:39 +05:30
sgaofen 7456081d2a fix(ssh): forward skill/config env passthrough over SendEnv (#14091)
The SSH backend spawned `ssh … bash -c` with no env forwarding at all, so a
skill's `required_environment_variables` (and `terminal.env_passthrough`)
reached local and docker shells but never a remote host — the skill reported
ready while every remote command saw the variables unset.

Names travel as `-o SendEnv=NAME`; values ride the ssh client's own
environment, so no secret ever enters the remote `bash -c` argv. Hermes
provider credentials stay behind the same blocklist docker uses, `.env`
fills in variables unset in the shell, and profile-scoped names missing
from the active scope are unset remotely under multiplex. The remote sshd
must `AcceptEnv` the names (documented).

A ControlMaster only relays the SendEnv names it was started with, so
passthrough commands ride a per-name-set master socket (verified live
against a real sshd: the shared master silently dropped every variable).

Salvaged from #14414 by @sgaofen.
2026-09-05 15:54:39 +05:30
Teknium 13e72fb205 Merge pull request #102986 from NousResearch/fix/browser-exec-packaged-chromium
browser_exec local mode drives the packaged Chromium instead of the user's Chrome
2026-09-04 08:42:27 -07:00
Teknium 9e13ac2f01 fix(browser): browser_exec local mode drives the packaged Chromium, not the user's Chrome
Symptom: with Browser Use mode (the default) and no cloud provider / CDP
override, browser_exec left BU_CDP_* unset, so the browser-use harness ran
its own local discovery — hunting for the user's INSTALLED Chrome on its
default profile. That path needs the chrome://inspect remote-debugging
toggle plus an "Allow remote debugging?" popup per run, is blocked outright
on Chrome >=136, and on a headless host (or one without Chrome) fails with
"chrome-not-running: no supported Chromium-family browser is running". The
built-in browser_* tools never had this problem: they drive the Chromium
Hermes installs, launched through agent-browser.

Change: `_resolve_backend_cdp` step 4 becomes the local ENGINE —
lightpanda when configured, otherwise `_resolve_managed_chromium_cdp`,
which runs `agent-browser --session <key> get cdp-url` through the legacy
`_run_browser_command` (Chromium preflight/auto-install, per-key session
cache, inactivity reaper, atexit) and exports the returned ws:// endpoint
as BU_CDP_WS. Each cache key (task, or bu-named-<session>) gets its own
Chromium, so named sessions are private and skip the own-tab preamble.
Running `get cdp-url` on every call also refreshes agent-browser's idle
timer (it never sees the harness's direct CDP traffic) and follows a
relaunch after the reaper closed the browser.

Live: before, browser_exec on origin/main → exit 1 "chrome-not-running";
after → new_tab/page_info succeed on a cold start, warm reuse, post-reap
relaunch, and a named session; a missing Chromium surfaces the install
hint instead of the harness's Chrome hunt.
2026-09-04 08:30:32 -07:00
Teknium c240e65399 fix(skills): self-improvement writes lessons, not incident logs
The skill review fork, the combined memory+skill review, and the curator's
consolidation pass all described references/ as the place for
"session-specific detail", and the curator's demote step said to move a
sibling's file under the umbrella. Followed literally over months that
produced one dev skill with a 100k SKILL.md dense in PR numbers and 443
one-per-session reference files, plus five sibling skills restating the
same rules and the repo's AGENTS.md.

The three prompts now share one shape contract: an entry is an imperative
rule plus one clause of why, stated once; no PR/issue numbers, dates, or
quoted chat as content; references/ is a small topical set extended in
place, never a per-session file; skills do not restate always-loaded
context. Consolidation is defined as distilling, and copying a sibling
verbatim under references/ is named as the failure. skill_manage's schema
carries the one-sentence version.

Two advisory linter rules make the shape visible in the tool result the
moment it starts to drift: incident-log-shape (PR/issue-number density in
prose) and references-sprawl (>60 reference files), the latter also run
on references/ writes. On the real before/after: the old skill trips both,
the consolidated one trips neither.
2026-09-04 08:15:22 -07:00
Teknium f1ccf436a2 feat(web): Perplexity Search API as a web_search + web_extract backend
Adds plugins/web/perplexity — a keyed-only WebSearchProvider over httpx:

- search: POST https://api.perplexity.ai/search (documented Search API),
  search_context_size=low so `snippet` stays description-sized;
  results[].snippet -> description, max_results capped at the API's 20.
- extract: POST /sdk/content/snippets — the query-relevant page-excerpt
  route behind `pplx content snippets` (the CLI's `content fetch` is
  deprecated upstream). web_extract has no query, so the URLs' path words
  serve as the relevance query; per-URL `error` entries survive a 200.
- Wired into the same touchpoints as the other keyed vendors: legacy
  backend set + credential ladder + availability probe (web_tools),
  registry preference walk, OPTIONAL_ENV_VARS, `hermes config`/status/
  dump key lists, nous_subscription direct-credential detection, setup
  summary, test conftests, docs.

Not a keyless-ring member (Perplexity has no anonymous tier). Related
closed PRs #9192 / #23981 / #45225 predate the plugin ABC.
2026-09-04 07:17:00 -07:00
Teknium a0be177aac fix(compat): pointers resolve to the object that MOVED, not a same-named stranger; stdin checker binds stdin= to the splatted definition
Review findings on #102117 (independent reviewer + itsflownium):

* hermes_cli.kanban_db.connect / connect_closing pointed at hermes_cli.projects_db (different DB, no
  board= parameter). The compat generator ranked candidate homes by path proximity when a name is
  defined in several modules. Now it requires shape compatibility with the BASE definition (same
  literal for constants, superset of parameter names for defs) and prefers the facade's own
  <stem>_* sibling. Same class fixed for tools.tts_tool.DEFAULT_XAI_BASE_URL (-> tts_tool_providers),
  and 17 constants/defs that had been pointed at same-named strangers (Matrix MAX_MESSAGE_LENGTH ->
  Signal's 8000, tts MAX_TEXT_LENGTH -> BlueBubbles', honcho/retaindb/supermemory *_SCHEMA -> another
  plugin's schema, ...) are now restored from BASE verbatim instead.
* send_yuanbao_direct (restored-def): body called adapter._outbound.send_direct, which HEAD moved to
  the sender; rewritten to adapter._outbound.sender.send_direct.
* COMPAT_MANIFEST.md states the scope explicitly: public top-level names only; private names and
  test monkeypatch seams are not preserved.
* scripts/check_subprocess_stdin.py: _splat_carries_stdin looked 30 lines ahead in the file text
  and was satisfied by an unrelated later stdin=; it now finds the splatted name's definition via AST
  and requires stdin inside that expression/body.

Tests: tests/test_compat_manifest_targets.py (pointer identity vs the facade's sibling; kanban
connect(board=) opens a Kanban DB, not projects.db; both FAIL on the previous layer),
test_subprocess_stdin_guard gains the false-negative probe, and the MoA -Q quiet-output contract
tests are back (tests/agent/test_moa_quiet_reference_output.py) against build_moa_facade.
2026-09-03 22:00:01 -07:00
Teknium ba030bc0db fix(test-seams): monkeypatch.setattr facade aliases — also patch the defining module (57 files)
Tests did monkeypatch.setattr(<facade module>, name) where name is now defined in a
sibling module and the production path reads the sibling's binding. Where production
reads through BOTH bindings the setattr is duplicated onto the defining module (import
added next to the existing alias import); where only the sibling reads it the target is
repointed. Seams whose production readers go through the facade are left alone.
2026-09-03 19:33:28 -07:00
Teknium b35836c928 fix(ci-fallout): repoint 3 more tests off old facade paths; packaging test tolerates sibling scratch root modules; compat lint also catches monkeypatch.setattr/patch.object on facade aliases 2026-09-03 17:45:53 -07:00
Teknium f43b976209 fix(ci-fallout): repoint the in-container docker test, two windows-only tests and the desktop exe-integrity patches to the defining modules; name bot_relay turn timeout/attempts as constants so the Desktop mirror test reads them instead of counting subprocess.run sites 2026-09-03 17:28:02 -07:00
Teknium 191e7c74a1 simplify(compat): terminal_tool — repoint 1 missed test patch target (_sudo_wrong_password_failure) to terminal_tool_sudo 2026-09-03 14:42:20 -07:00
Teknium 69eb1feb3e simplify(compat): tools/browser_tool — drop the legacy _PROVIDER_REGISTRY test-override shim (+_is_legacy_provider_registry_overridden), repoint 1 test to the agent.browser_registry seam; repoint 1 missed supervisor import 2026-09-03 14:25:33 -07:00
Teknium da7ee6353e simplify(compat): tools/browser_tool tests — repoint 56 test files from tools.browser_tool.<name> to the defining browser_tool_* sibling (patch where the name is looked up) 2026-09-03 14:18:25 -07:00
Teknium 7a33369e81 simplify(compat): interrupt — drop _ThreadAwareEventProxy/_interrupt_event legacy alias, repoint 2 test files
No runtime consumer read the proxy (terminal_tool/environments call is_interrupted()/set_interrupt()
directly); its only users were tests patching tools.interrupt._interrupt_event, which had no effect on
the code under test. tools/terminal_tool.py's own re-export of the name is owned by another worker.
2026-09-03 14:00:59 -07:00
Teknium c93ace77c2 simplify(compat): config/runtime_provider/plugins/commands/secrets_cli/kanban — drop 96 re-exports (incl. PEP 562 facades) + 3 aliases (get_pre_tool_call_directive/_block_message, get_telegram_handler_factories), repoint 56 callers + 50 test files 2026-09-03 14:00:17 -07:00