mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 13:26:40 +00:00
08f954e505cab50d6464f84ba767bdf292e8bed2
120
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f219708926 |
feat(tts): fold voice search into text_to_speech's ElevenLabs schema (find_voices)
Teknium: the ElevenLabs variant of text_to_speech is where voice search belongs, not a
separate tts_voices tool. The ElevenLabs schema now carries voice_id plus a find_voices
object (text optional there); other providers' schema is byte-identical to main.
Live search findings folded in: the library is the default source and sorts by a year of
usage (most-used first); use_case (e.g. characters_animation) and sort filters; library
voices with a custom credit rate show credit_rate; the description tells the model the
library matches words literally, so a role word that finds nothing ('dwarf') falls back to
describing the sound ('gravelly old' + filters).
|
||
|
|
c975831fbc |
feat(tts): let the agent pick ElevenLabs voices (voice_id + paginated tts_voices search)
With tts.provider: elevenlabs, text_to_speech gains an optional voice_id and a new tts_voices tool searches the user's account voices (/v2/voices) or the public Voice Library (/v1/shared-voices) with query/gender/age/accent/language filters and an opaque next_page cursor. Both are gated on the ElevenLabs provider (dynamic schema override + check_fn), so every other provider's surface is unchanged. A per-call voice_id never rewrites config; a stale one sent under another provider is ignored with a warning. |
||
|
|
7ba9d256bd |
docs(browser): browser_cdp and browser_dialog still need a configured CDP endpoint
The tools reference and the browser page said all four browser tools work on launched and cloud sessions. browser_cdp and browser_dialog are gated on a static endpoint (_browser_cdp_check: /browser connect, BROWSER_CDP_URL or browser.cdp_url) and are absent otherwise. The pages (and their zh-Hans mirrors) say so again; "Requires environment" is back to "CDP endpoint". Review: https://github.com/NousResearch/hermes-agent/pull/143146#discussion_r4351613180 |
||
|
|
0b69853adf |
docs(browser): browser_exec is the only browser path; Camofox is a plugin
Rewrite the browser feature page, configuration, tools/toolsets/env-var/pm references, docker, windows, bot-screen, termux/installation and the secondary developer-guide/integration/feature pages (EN + zh-Hans): drop agent-browser, the built-in browser_* tools, browser.backend and /browser use; document browser.executable_path / browser.user_data_dir / browser.chromium_args and the AGENT_BROWSER_* migration; point Camofox users to 'hermes plugins install camofox'. |
||
|
|
f5ea924b7e |
docs: remove the initiate-setup and first-task skills and the setup docs
Both optional skills go, and generate-skill-docs.py prunes their pages, the
optional-skills catalog rows and the sidebar entries (the sidebar would point at
missing pages otherwise). slash-commands, toolsets-reference, delegation,
tools-reference, configuration's Onboarding section and the desktop setup-chat
section return to their
|
||
|
|
72e7085e9e |
feat: set_theme replaces apply_layout and reports what the desktop applied
set_theme (desktop_ui, deferred like apply_layout was) sets the accent,
light/dark mode and layout preset, or with `picker` shows an accent or
layout card and waits up to 300 s. apply_layout emitted a layout.apply
event and returned success whether or not the renderer applied anything;
set_theme asks the renderer through the theme.apply server request and
returns its answer, so an unknown preset or a chat that is not on screen
comes back as an error with the available ids.
theme.pick waits for a person; the backend tells a dismissed card
({cancelled}) from an expired one ({timed_out}). The tool description
lists the Simple and Advanced preset ids from one constant, which fixes
the old description that named neither sidebar preset. The layout.apply
event and its renderer handler are deleted with the tool.
|
||
|
|
aa915ec02a |
fix(connectors): display costs fewer tokens, drops said, reports connections on timeout
Sid's review of the display action: - The display text in the manage_connections description is cut to about 60 tokens; every session (cron and messaging included) pays for it. - "said" is removed from the wire (ConnectionAnswer), the backend, the description, the receipt copy and the renderer. Nothing sent it; typed answers for this card arrive with the composer registry later. - The 300 s deadline now returns connected and newly_connected with timed_out: true, so an app the user connected before walking away is reported. The receipt names it. |
||
|
|
b1d0fe6060 |
feat(connectors): manage_connections display waits on a directory card
Add action "display": on a desktop session the call opens an operation
with no targets, emits connection.request with display.suggested, and
holds the tool thread until Continue, a typed answer (said), the 300 s
deadline or /stop. It returns connected / newly_connected / suggested,
computed from the gateway list read at the start and at settle, or
{said} / {timed_out}. Off desktop it returns the status list and says
where to connect.
The card reuses the existing operation transport: connection.respond by
op_id settles it; ConnectionAnswer gains "said". Connecting from the
card runs the Connectors page's account operation, so the two never
share the session's one open slot. The managed_tools gate is unchanged.
|
||
|
|
1fad8eb3dd |
docs: multiple named connector accounts
Covers account names, no default account, the agent telling the user the name it chose, connector_alias on tools and account on results, rename, reconnect keeping the old account retired, and where to manage accounts (desktop, TUI /connectors, hermes connectors, Nous Portal). |
||
|
|
71704ffb49 |
refactor: move Spotify out of core into the spotify catalog plugin
The spotify toolset (plugins/spotify) and its loopback-PKCE login (hermes_cli/auth_spotify.py)
now live in NousResearch/hermes-spotify, installed from the plugin catalog. Same pattern as the
Home Assistant move (
|
||
|
|
1598b9007c |
onboarding: the account's full name reaches the model only when the user picks it
The name was in the /initiate-setup facts block and in the scripted name card's saved arguments, so it left the machine even when the user typed another name or skipped the card. facts() drops it, and setup_choose adds the suggested row itself after the model's call is saved, the way it fills the tour and fork rows. The result's label carries the name when the user picks it. |
||
|
|
8b89b68fb4 |
Merge origin/main into sid/onboarding
Brings the onboarding branch up to main |
||
|
|
6fec745d45 |
fix(tour): gui_tour rejects preset with steps, and the docs describe the built-in tour (review)
tools-reference.md: gui_tour row and Tours section describe bare start, preset quick|full, and custom tours. tour_tool.py: preset together with steps is an error instead of being ignored. test_tour_tool.py: through the registered handler, an invalid preset and preset+steps are rejected and a valid preset reaches the request. |
||
|
|
c2b13dc3d8 |
feat(desktop): every desktop chat can install catalog plugins; tools gated by session platform
Toolsets that exist only for one kind of session now say so on their spec:
`platforms=frozenset({"desktop"})`. `TOOLSET_SESSION_PLATFORMS` is derived
from the specs that carry the key, `all` / `*` / no selection skip those
toolsets, and `toolsets.agent_tool_drops(agent)` removes their tools from any
agent whose own session platform is not listed. `agent_init._load_tools` and
`tools/mcp_tool_agent.py` (MCP refresh and prefix restore) call that one
helper. `tool_search._DIRECT_SURFACE_TOOLSETS` is derived from
`CLIENT_SURFACE_TOOLSETS` and `TOOLSET_SESSION_PLATFORMS`.
This replaces the profile-role gate: `profile_role_toolsets`, the role code in
`_with_session_toolsets` and the explicit-pin branch, the strip step in
`_select_tool_names`, and main's `setup` toolset (whose only tool moves out)
are deleted. `manage_catalog` moves into a new `catalog` toolset, which the
desktop client folds into every desktop chat like `project` and `desktop_ui`.
It is deferred behind tool_search by default (`config_defaults` defer list).
The shared-metrics schemas' toolset enum follows (`setup` out, `catalog` in).
The install card works outside the setup chat (#129650, with review fixes):
- Parallel installs: `_set_plugin_enabled` holds a per-home lock around the
read-then-admit section, so rows enabled at once no longer refuse each other.
- A catalog plugin already on disk and not enabled is enabled in place, so Try
again after a refused enable can finish.
- A failed catalog row counts as resolved (`contract.resolves`), so it no longer
holds the turn to the 300 s deadline. The snapshot carries `resolved`, and the
card reads it instead of a hand copy of the backend's rule.
- Each card draws its own operation by op id, because models reuse call ids.
- The row carries the approved non-secret choices (`approved`: force, enable,
ref) and the chosen profile, so Try again after the card settled repeats them.
- Install phases are ids (`InstallPhase`, wire `CatalogInstallPhase`) and the
installed-row notes are fields (`enabled`, `missing_env`, `server_errors`,
`already_installed`, `requires_hermes`); the card words them in English
strings (translations follow later). Failure reasons stay raw text, and
`detail` keeps the English notes for the model.
- Installs and search target the calling chat's own home, carried as a path;
the row's profile name is only a label, with no `'default'` fallback in
Python or TypeScript.
- `dashboard_install_plugin` is split into `_resolve_source`, `_place_tree` and
`_enable_placed`.
Desktop English strings for the card move to `en_catalog_install.ts` /
`types_catalog_install.ts`, which offsets the growth of `en.ts` and `types.ts`.
|
||
|
|
2b3afee3fe |
fix(tools): a task's own patch keeps its full-file write baseline
A deployment reported that write_file -> read_file (full) -> patch -> write_file refuses the last write with stale_write_blocked and "exists but this task has not seen its full current content", in 50 of 50 trials. No other writer took part. When the model does not reread before the turn ends, the refusal surfaces as the "File-mutation verifier: N file edit(s) FAILED" footer. Mechanism. The write_file baseline is an exact file version: (st_dev, st_ino, st_size, st_mtime_ns, st_ctime_ns, sha256) (tools/file_tools_read_tracking.py _file_version, _mark_full_write_baseline, _has_full_write_baseline). write_file_tool marks a fresh baseline from the bytes it wrote. patch_tool only ran _note_edited and _reset_patch_failures, so the task's own patch changed the version and left the baseline it held stale. _stale_overwrite_blocker then treated the task like an external writer. Invariant: after a task's own successful patch, the task may replace the file whole exactly when it held a current full baseline for the bytes the patch read and the bytes the patch wrote are what is on disk. Lineage. |
||
|
|
3724ace0c8 |
Merge remote-tracking branch 'origin/main' into feat/store-guest-onboarding
# Conflicts: # apps/desktop/src/app/session/hooks/use-message-stream/gateway-event/message-stream.ts # apps/desktop/src/app/session/hooks/use-message-stream/gateway-event/types.ts # apps/desktop/src/app/session/hooks/use-message-stream/index.ts |
||
|
|
0f7a95f16d | docs: Home Assistant moves to the plugin catalog | ||
|
|
4efdfb6d84 |
Merge remote-tracking branch 'origin/onboarding/next' into feat/store-guest-onboarding
# Conflicts: # apps/desktop/electron/window-focus-policy.ts # apps/desktop/src/components/assistant-ui/inline-preview-directive.tsx # apps/desktop/src/lib/chat-messages/parts.ts # apps/desktop/src/plugins/hermes-bots/types.ts # apps/desktop/src/sdk/index.ts # hermes_cli/plugins_cmd_install.py # hermes_cli/profile_distribution.py # plugins/web/keyless_mcp.py # tui_gateway/methods_profiles.py # tui_gateway/server.py # website/docs/developer-guide/relay-shared-metrics.md # website/docs/user-guide/features/web-search.md |
||
|
|
7e53b3ef82 | chore(memory): remove bundled honcho provider; install from the plugin catalog | ||
|
|
5b345904ab |
docs: escape <machine> so the docs site builds
MDX reads a bare <machine> as an unclosed JSX tag. #129646 left two of them in prose (onboarding-recommendations.md, the setup_choose row in tools-reference.md), and the Docusaurus build fails on onboarding/next. Write them as <machine>, the form the generated skill pages use. |
||
|
|
a2065efd51 |
fix(setup): each setup card answers only its own call (#130062)
One Skip broke a tester's whole onboarding. The provider named every tool call "call_0", and the desktop bound each setup card to its request by call id alone. After the apps card was skipped, every later card with "call_0" showed that old "Skipped" answer instead of its picker. Composer text was also saved as a pick, so "hey" became a plugin and a complaint became the layout. - The agent renames a tool call whose id is already used earlier in the session (call_0 -> call_0_d2), before the call is stored or run. Ids already in history never change, so the prompt cache holds. - The card's request binding is keyed by session, message and call id. - Composer text on a card with fixed rows counts as a pick only when it names a row by id or label. Otherwise setup_choose returns outcome "typed" with the words in `said`, records no card facts, and tells the model to reply and send the card again. The settled card shows the typed words. |
||
|
|
ff4760bfbd |
The first task after setup connects the picks and finishes something small within minutes (#129646)
* feat(onboarding): the first task chat delivers a small result in minutes Live runs on an RTX Spark showed the task chat surveying the whole machine, asking a plan question, and installing the picked plugins 11.5 minutes after setup. Its suggestions ignored the picks and the scan, and the handoff text said "Call me Sid." and "Start from this; look at my computer again...". - New optional skill first-task. start_chat appends it to the task chat's first message, after the ask and the "What setup learned" block, with a JSON block of the connector and plugin ids picked in setup. No system prompt changes. The history projection cuts the message at its marker, so no surface shows the skill. Its rules: connect and install the picks first, work visible in 1 minute and a finished result in 5, big asks cut to a first slice, three options only for a vague ask, machine setup as a short card interview. - setup_choose records the picked ids and the fork pick. The fork card is filled when shown: two first tasks from the picks and the scan (a daily brief from the picked apps, "Install a few apps for this Spark", a small page), then "I have something in mind", "Help me set up this <machine>", "Let's figure it out together". The machine-first "Something else" step, "automate" and "skip" rows are gone; "figure" hands off at once. - The handoff template is the ask only; no "Call me", no install or plan paragraphs. start_chat drops the "Start from this" line. - initiate-setup SKILL.md keeps only the setup beats (20.5 KB -> 10 KB). - Desktop shows the "What setup learned" block folded under the first message; the model still gets the full text. * fix(setup): one unexpected shutdown reads as singular |
||
|
|
59cbf85cca |
Setup cards name the pick, skip an empty apps or plugins list, and wait for the intro (#129383)
* fix: setup cards tell the model the name the user saw, and an empty apps or plugins list shows no card
The accent card showed "Ultraviolet", but the result only carried "#8a2be2", so
the model guessed a CSS name ("BlueViolet, done."). The card now answers with the
label of each picked row next to its id, the backend passes it through for the
app-owned lists it cannot name itself (accent, theme, layout, connectors,
plugins), and the tool description tells the model to say the label.
A session with no Nous identity (own key, guest sign-up rate limited at boot)
got an apps card that said "This list isn't available right now". Now
setup_choose kind:"connectors" checks connectors_available() in the setup
session's own profile scope before any card: with no identity and the free tier
on, it makes one guest sign-up attempt through ensure_portal_identity (the mint
memo's cooldown still applies), and if connectors are still closed it returns
no_answer with a one-line notice and the next card, without showing a card. An
empty plugins list returns the same way. The skill says one line about the free
Nous account and moves on.
The renderer re-reads a cached "unavailable" connector catalog when the card
mounts, so a guest minted by the card is not hidden behind the prefetch taken at
the start of setup.
* fix: the setup intro holds the thread until its spring settles
The backend prelude writes the first assistant row and the name card while the
intro copy is still typing. The copy's cover uses the chat surface colour, which
is transparent under glass, so the card showed through mid-motion. While the copy
plays ($introHoldsThread, true only for the 'playing' stage), the thread under it
is hidden; it shows when the spring settles, and at once with reduced motion
(the stage goes straight to 'landed'). The backend prelude is unchanged: its
tool call waits for the answer.
|
||
|
|
fe50fc8b60 |
Setup cards: tour and fork always arrive with their rows, apps preselected from the scan, honest skill (#129344)
* feat(setup): the backend fills the tour and fork cards and preselects scanned apps The welcome model fills only the required parameters of setup_choose, so cards that needed options arrived as bare text boxes in play 4. options is now required ([] means the app's list, or free text for a question). Two new kinds, tour and fork, take their rows from the backend whatever the model sends. The fork rows come from the facts the /initiate-setup turn embedded, recorded next to the setup state; "Something else" on a machine-first fork opens the rest in the same call. The connectors and plugins cards start with the apps the scan saw in use, and Blender when it is here, already picked. The result names a picked row (label), so the settled card shows labels for rows that are not in the call's args. * fix(setup): the setup skill answers honestly about the scan and always reaches a handoff Card lines carry options and use the app-owned tour and fork kinds. When asked what Hermes knows, the model answers from the facts and names the scan instead of denying it. A skipped fork goes to a first-task card built from the scan, a second skip offers one concrete task, and a card left for an off-flow question is re-sent. Dead fork-building and fallback text is cut, which keeps the skill under 25 KB. * fix(setup): card facts per setup conversation; the backend steers each beat and carries the handoff The saved card facts were one file per setup profile, so a second /initiate-setup overwrote the first conversation's fork rows. They are now keyed by the session id under the bound Hermes home, and setup_choose reads its own session's block. A fork card with no recorded facts (skill loaded through the inline-shell hook) collects them fresh. setup_choose now returns `next` for the flow's following step (the next card's exact arguments, the skip ladder from the fork on, a re-send of the pending card after typed off-flow text) and, from the fork, `handoff`: the message parts and the plan, cut to this machine, from templates/handoff.md. That moves about 6 KB of handoff and plan text out of the first turn. Rows the model sends for tour, fork and the new machine_use kind are dropped before validation, so a malformed copy can no longer fail the card. The connectors and plugins cards are always multi-select, so preselected rows are never lost to a single-select confirm. The off-desktop voice rule is back in the skill. * docs: regenerate the initiate-setup skill page * feat(initiate-setup): one "I noticed" line before the fork shapes the first tasks |
||
|
|
f9102e4ca4 |
Setup scan stays off backend globals; audit lint and RPC fixes (#129341)
* fix: userscan tmux and zellij socket probes pass the /tmp literal check The four lines read where tmux and zellij put their sockets (and, on macOS, measure the user's own temp dir). The POSIX fallback for those directories is /tmp, so the literal is the fact being probed, not a scratch path. Mark each line so the blocking lint step in lint.yml passes. * fix: setup-profile and start_chat RPCs no longer block the RPC reader onboarding.ensure_setup_profile / ensure_setup_session / reset_setup_profile can run create_profile (a skill-tree copy), state.db writes and the first import of the setup scanner. session.start_chat creates a session and runs prompt.submit. Inline on the reader thread they left session.interrupt and approval.respond unread in the pipe, the failure the _LONG_HANDLERS comment names. profiles.create was already on the list for the same reason. * fix: userscan children run with stdin closed and the served profile's env The /initiate-setup builder runs the user scan on a backend thread, so every child the scan spawned inherited the JSON-RPC stdin pipe (#14036) and the launch profile's os.environ. HostAccess.run, copy_locked and the PowerShell batch now pass stdin=subprocess.DEVNULL, and every spawn takes its env from HostAccess.child_env(). host_facts passes served_profile_child_env() into the run; the standalone run.py keeps the process env. check_subprocess_stdin.py now scans optional-skills/: the backend loads initiate-setup's scripts in-process. The nested-dir skip matches paths relative to the context dir, so a skill's own scripts/ folder is in scope. Three model-run optional-skill scripts it flagged get stdin closed. * refactor: split the userscan spec modules over 2,000 lines by topic Six spec modules under initiate-setup's userscan passed the 2,000-line gate. Each keeps its shared helpers and first families; the rest move verbatim into <stem>_<topic>.py siblings that import the helpers they use from the base module: apps_dev -> apps_dev_agents, apps_dev_devenv browser_files -> browser_files_comms, browser_files_content usage_gaming -> usage_gaming_games, usage_gaming_hardware linux_system -> linux_system_security, linux_system_hardware linux_apps -> linux_apps_dev, linux_apps_browser, linux_apps_files darwin_apps -> darwin_apps_dev, darwin_apps_browser, darwin_apps_files linux_system keeps health and usage: install_age reads usage's _wtmp and usage reads health's _kdump_dirs. References through the ad./bf./ug. aliases point at the module that now defines the name. specs/__init__ imports each sibling right after its base, so per-OS registration order and the registry keys stay the same. No behaviour change: `run.py --list` is byte-identical before and after, and a temp-home scan under --os darwin/linux/windows gives the same fact statuses, errors and insights. * fix: the setup scan never patches process globals or sys.path in the backend The /initiate-setup builder runs the user scan on a backend thread. It put the scanner dir at sys.path[0] of the backend (so a top-level `run` or `userscan` import anywhere resolved to the scanner), and it loaded host.py's deny_reads_under (swaps builtins.open, io.open, sqlite3.connect and subprocess.Popen) and override_env (rewrites os.environ HOME and HERMES_HOME) into the backend's import graph. - userscan/cli.py now holds override_env, the read guard, the aggregate HostAccess and scan_accounts. Only run.py imports it; runner.run() no longer retargets os.environ (overrides stay l0 data), and run.py applies override_env around run() for --home and friends. - The package uses relative imports, and host_facts loads it once per process as initiate_setup_userscan, without touching sys.path. Live: after an in-process scan sys.path, os.environ and the four patched callables are unchanged and no top-level userscan module exists (before: sys.path[0] was the scanner dir and userscan.host was importable). `run.py --list` is unchanged; --all-users and --home give the same output. * refactor: userscan runner runs each layer as a named phase runner._run was one closure-heavy function at cyclomatic complexity 66. Its state moves onto a small _Pass object whose run_l1 / run_l2 (with _run_python_l2) / run_l3 methods each hold one layer, moved verbatim; _run composes them and builds the output. Highest CC in the file is now 21 (run_l3). No behaviour change: temp-home scans under --os darwin/linux/windows give the same fact ids, statuses, methods, tiers, coverage, insights and output keys as before. * chore: name the setup intro flag writer, document gui_tour presets, trim media.ts - hermes_cli/setup_profile.py::mark_seen becomes mark_intro_seen. It writes the setup profile's intro state; agent/onboarding.py::mark_seen flips a config.yaml hint flag, and tui_gateway uses both. The RPC name onboarding.mark_seen is unchanged. - tools-reference: gui_tour's `start` without `steps` runs the built-in tour (`preset` quick or full, default full). - apps/desktop/src/lib/media.ts: drop the blank line at EOF (git diff --check). * docs: describe the setup chat's automatic machine scan The scan runs without a consent step when the desktop app creates the setup profile (the owner kept it automatic). The desktop user guide now says where it runs (the backend's machine), what it reads (T1: counts and categories; finer values are dropped before storage), what reaches the model (the derived fact block in the first setup message, sent to the setup chat's inference provider) and where the cache lives (insights/profile.json in the setup profile's home, 0600, 24 h). The skill's own docs page is generated from SKILL.md, which is the model's prompt, so the user-facing text lives in the desktop guide. * fix: overlapping setup-profile RPCs no longer race on the RPC pool ensure_setup_profile, ensure_setup_session and reset_setup_profile moved off the reader thread, so two kickoffs could overlap: both saw no setup profile, and the losers failed on the half-made profile (5 of 6 barrier-started calls returned ENOENT), or both saw an empty setup chat and seeded it twice. One module lock restores the one-at-a-time order; the prescan stays outside it. * fix: the Linux locale probe reads date formats in a child, not via setlocale locale.user ran setlocale(LC_TIME) on the scan thread inside the backend. setlocale is process-wide and not thread-safe, so strftime in other session threads briefly used the user's LC_TIME. The probe now runs 'locale d_fmt t_fmt' under LC_ALL=<locale> through HostAccess.run (new env overlay on child_env), after checking 'locale -a', because locale prints C formats for a locale that is not installed. * docs: link the setup scan from the /initiate-setup and onboarding references The desktop guide's scan section now says which search-engine names are kept and that the scan runs in the CLI and the gateway too. The /initiate-setup row and the profile_build bullet link to it. |
||
|
|
8feae75347 |
Apps and plugins: drop setup_choose intent, manage_catalog out of setup, cards set up later (#129245)
* fix(setup): manage_catalog leaves the setup chat; installs go to the caller's profile
- The setup profile disables the catalog toolset. The desktop session
folded it in as a client-surface toolset, so manage_catalog was still on
the setup chat's eager tool list.
- Delete default_target_profile: a catalog install and search target the
calling chat's own profile, the setup profile included.
- /initiate-setup still needs "the launch profile when run from the setup
home"; that moves to hermes_cli.setup_profile.primary_profile().
- The manage_catalog description and the reference docs drop the setup
chat exception.
* feat(setup): drop setup_choose intent; apps and plugins cards say they set up later
- setup_choose loses `intent` (the now / when needed / just note it choice)
in the tool, the wire contract and the cards. The result is
{outcome, picked}. Contracts regenerated.
- The apps and plugins pickers carry one footnote, "We'll set these up
when you start.", replacing the plugins-only "Nothing connects or
installs yet" note. Other locales translated; the desktop key list is
regenerated (it was also behind en.ts on appTour and sharedMetrics).
* fix(desktop): an install card stays drawn when a connect card follows it
The renderer keeps one connection operation per session. When a turn asks
twice (a manage_catalog install card, then a manage_connections connect
card), the second operation replaced the first in the store, and the
settled install card fell back to a plain tool row.
A settled operation that the next one replaces now moves to a store keyed
by its tool call, and the catalog and connector cards read their own row's
operation from there. Both cards stay stacked in the turn.
* fix(setup): setup_choose inline calls stop passing intent; /initiate-setup reads the launch home at call time
The inline executor still mapped intent into setup_choose_tool, which no
longer takes it, so every desktop setup_choose call raised TypeError.
primary_profile now gets server._launch_home() instead of the import-time
_hermes_home.
* fix(desktop): keep one card per kind: the latest install card and the latest connect card
The replaced-operation cache kept every settled operation by tool call, so
consecutive manage_connections calls drew one connect card each. It now
keeps, per session, only the settled operation of the other kind (install
vs connect) that the current one replaced. An install card and a connect
card stay stacked in either order, and a later call of the same kind still
takes over its card. clearConnectionRequest clears both caches, and the MCP
setup card reads the same per-row selector, so sessionConnectionRequest is
gone.
|
||
|
|
b3411f4d8c |
feat(start_chat): a handoff from the setup profile marks onboarding complete
When start_chat runs from the marked setup profile's chat and opens the new chat in another profile, the backend now writes completed_at and intro: seen into the setup profile's onboarding marker. The write goes through a new hermes_cli.setup_profile.mark_completed(), next to mark_seen(), so onboarding.state reports completed_at unchanged. Calls from any other profile write nothing. A call from the setup profile into itself writes nothing. Rejected calls write nothing. A reset of the setup profile still writes the fresh state, so it clears completed_at. The tools reference row for start_chat states the new effect. Linear: NS-1018 |
||
|
|
b321a7be69 |
feat(desktop): setup_choose question cards and the start_chat handoff card
The setup bot's two tools now render as native cards in the desktop chat.
setup_choose goes through the clarify card path instead of a second card:
the renderer answers the setup_choose server request, parks it in the
clarify store (so the needs-input dot, the notification, the composer's
type-to-answer skip and resume restore all cover it), and draws it with
the clarify card's shell, key handling and confirm bar. Kind question
uses the clarify question block. Accent, theme and layout reuse today's
onboarding swatches, the settings mode labels and the layout preview
cards, and apply the pick live. Connectors (read on the account owner,
since the setup profile has no manage_connections) and plugins use the
onboarding chips, with a now / later / save control per picked row when
intent is on. The answer returns {picked, intent} on P15's wire; Skip
returns {} (cancelled).
start_chat shows the new chat's title and profile with an Open button.
A live completion moves the user into the new chat only when the calling
chat is in the foreground; history and background completions never
move them. start_chat joins the backend lifecycle UI set so the card
shows with tool progress off.
Open decisions for review: accent and theme picks apply live only (the
accent override is not persisted, and the theme mode lands on the setup
profile); the theme card offers light, dark and system.
|
||
|
|
14dbaec0ab |
fix(catalog): an install from the setup chat targets the launch profile
Sid's decision: the setup chat installs for the profile the user will work in, where start_chat sends the task, not into the setup profile. - default_target_profile() is the chat's own profile, except when that home carries the setup-profile marker; then it is the launch profile (server._hermes_home). Every other chat keeps the chat's own profile. - prepare() and search() use it, so the card row, the install and search's installed_in agree. The card still shows the target, and Advanced still picks another profile. - The manage_catalog description and the reference docs say where the setup chat installs. |
||
|
|
b5eb5fd5c4 |
feat(catalog): manage_catalog is a desktop catalog toolset that installs into the chat's profile
manage_catalog moves out of the setup toolset into a new `catalog` toolset. Desktop sessions get it the same way they get project and desktop_ui: the client surface folds it in, TOOLSET_SESSION_PLATFORMS drops it everywhere else, and `all` does not include it. It stays a direct-surface toolset, so the default tools.tool_search.defer list (which now names manage_catalog) decides deferral and a user defer list takes control. Installs and the search's installed flag now default to the profile of the chat that calls the tool, not to `default`. The Advanced modal can still name another profile. The setup toolset stays, empty, for setup_choose. The metrics schema toolset enum gains `catalog`, and the toolsets, tools and tool-search docs follow. |
||
|
|
e0e0505989 |
feat(start_chat): any desktop agent can start a visible chat through a native tool
The setup chat handed off to a new chat through about 600 lines of renderer code that always built into the default profile. This adds start_chat, a backend tool that does the same job for any desktop agent. - tools/start_chat_tool.py registers start_chat (message; optional title of at most 40 characters; optional existing profile, default the caller's own). The handler late-imports tui_gateway.start_chat. - tui_gateway/start_chat.py finds the calling session from HERMES_UI_SESSION_ID, rejects an empty message, a long title or an unknown profile, and never creates a profile. It creates the session with the caller's source and transport, inside the target profile's runtime scope, so the caller's display config does not leak into the new chat. It latches onboarding.seen.profile_build_offered in the target profile and submits the first message through prompt.submit. The call is not idempotent and keeps no receipt. - The start_chat toolset is desktop sessions only (keyed on the session platform), not in "all", not in `hermes tools`, stays direct instead of deferring behind tool_search, and is blocked for subagents. - Docs: toolsets and tools reference rows, and the subagent block list. |
||
|
|
16bec86daa |
feat(setup): setup_choose asks one question or picker card in the setup chat
The setup profile needs one tool to ask the user a question or show a
picker (accent, theme, layout, connectors, plugins) as a card. Today the
runbook emits ::onboarding and ::ask directives that the app parses out
of the model's text.
setup_choose joins the setup toolset, so it is desktop-only and direct
(never deferred unless a defer list names it). It validates kind,
question and up to 12 {id, label, detail?} options, and passes
options=null through so the app can show its own list. It reaches the
renderer as a setup_choose server request, bridged in
tui_gateway/agent_callbacks.py with the clarify wait timeout. The result
reports submitted (picked, plus intent now/later/save per row for
connectors and plugins), cancelled, or no_answer. It stores nothing.
The wire contract is in tui_gateway/contracts/server_requests.py and the
apps/shared contract files are regenerated. The desktop card and fixed
lists come later. The ::onboarding/::ask directives stay for now.
|
||
|
|
7182a214bb |
fix(setup): gate the setup toolset per session platform on every surface
Review follow-ups to the role deletion. - The setup toolset reached any surface that read platform_toolsets.cli (classic CLI, hermes -z, kanban workers), and "all" / "*" / a None selection pulled manage_catalog into every profile. Now "all" skips toolsets in TOOLSET_SESSION_PLATFORMS, so they arrive only when named, and agent tool loading drops their tools unless the agent's own platform is listed (toolsets.session_platform_tool_drops, applied in _load_tools and in the MCP refresh / pin restore paths). The name filter in tui_gateway's _with_session_toolsets is gone: one gate. - The focus coding posture replaced the configured list, so the guide in a code workspace lost the setup toolset. The posture path now carries the config's session-gated toolsets. - ensure_setup_profile deletes the new profile if a step after create_profile fails, so a retry does not leave unmarked orphans. - Desktop handoff recovery waits until the active session is bound to the selected guide chat, so it records the guide's profile and runtime id after the gateway swap, not the previous profile's. - Delete the renamed catalog test (lane rule) and correct the reference docs. Linear NS-1014. |
||
|
|
c3f85bc54f |
refactor(setup): delete profile role; the setup profile is a marked, plain profile
The setup toolset was reserved for `role: setup` in profile.yaml, resolved by toolsets.profile_role_toolsets, and stripped from every other profile in model_tools._select_tool_names. Onboarding also wrote that role into a user's existing unmarked hermes-setup profile and adopted it, and cloned the literal default profile. Now there is no role anywhere. The setup profile carries a dot-file marker (.setup-profile.json) at its root, and onboarding finds it by that marker. A user's own unmarked hermes-setup is never touched: onboarding makes hermes-setup-<word> instead. The new profile is cloned from the launch profile, and its own config enables the setup toolset in platform_toolsets.cli. The session resolver keeps the toolset only for desktop sessions (toolsets.TOOLSET_SESSION_PLATFORMS). Copies (clone-all, import, distribution install) drop the marker, as they dropped the role. The role field also leaves ProfileInfo, the profiles.list and dashboard rows, the onboarding.ensure_setup_profile result, the gateway contracts, and the desktop ProfileInfo type. The desktop handoff recovery check no longer gates on the roster role; the guide handoff receipt keyed by the stored session already identifies the guide session. Linear NS-1014. |
||
|
|
5eea87882a |
Clarify: one question shape and one result shape on every surface (skip, cancel, timeout and undelivered are told apart) (#127760)
* refactor(desktop): split clarify-tool.tsx into a clarify/ folder
Pure moves, no behaviour change. Parsing, the question-card core
(shell, choice rows, question block), the delivery watchdog, and each
pending/settled card get their own file so the core can be reused.
* feat(clarify): one questions[] shape with per-question status and one outcome
The clarify tool now takes only questions=[{question, choices?, multi_select?}].
A wrong shape is a tool error that names the right one, so a model that
sends the old top-level question/choices corrects itself on the next call.
Every result has the same shape on every surface: each response carries
status (answered, skipped, unanswered) with user_response null unless
answered, and the result carries one outcome (submitted, cancelled,
timed_out, undelivered) plus an optional surface-written notice. The
timeout and cancel sentences that used to pose as the user's answer are
gone, and the compressor reads status instead of matching their prefixes.
The callback contract is callback(questions) -> {answers, outcome, notice?}
(None = skipped, missing qid = unanswered). tui_gateway settles a batch the
same way for the last lock, a cancel, an interrupt and the deadline, and
clarify.lock keeps a null answer as a skip. A multi-select answer that is
not a JSON array counts as one typed ("Other") answer. The tool-row preview
reads the first question.
* feat(clarify): every surface asks through the one question card
CLI: the batch panel is the only panel; Enter on an empty field skips a
question, Ctrl+C cancels and keeps the locked answers, the deadline returns
timed_out. -q and -z return undelivered with a no-user notice.
Ink TUI: the single-question prompt is gone; the card supports multi-select
(Space or a digit toggles, Enter locks, typed Other text joins the picks),
an empty submit skips, Esc cancels, and "Batch" names are dropped.
Desktop: the single-question card is deleted and its keyboard handling moved
into the one card (arrows, letters and digits, auto-advance, Enter picks then
confirms). Confirm enables at one answer; blank questions lock null; Skip and
a composer message cancel; the settled card shows No answer for unanswered
questions. The bots room card follows the same contract.
Messaging: one card per question with a "Reply skip to skip" line in every
locale; timeouts and delivery failures map to timed_out and undelivered.
* fix(clarify): cancelled for a released messaging card, labels win over the skip word
A prose reply to a messaging choice card, /new and session end released
the wait with "", which read as timed_out. They now resolve with a
CANCELLED marker and the tool gets outcome cancelled.
A typed reply that matches a choice label ("Skip") now resolves to that
choice; the skip word applies only when no choice matches.
The bots room card sends the picked labels as they are; the tool already
strips the recommendation label. Unused OUTCOMES and a new docstring and
comment are removed.
* fix(tui): re-editing a multi-select answer keeps its picks
The Ink card stored a multi-select answer as display text ("A, B"), so
revisiting the question put the whole string into Other and re-locked it
as one item. The answers map now keeps the raw JSON array, the card
restores the picks on revisit, and only the display lines format it.
Comments that earlier edits reworded are restored to their original
words, minus the phrases the change made wrong. The compute-host clarify
lock accepts None for a skipped question.
* test(clarify): align three checks with the one-answer confirm and the regenerated keys
The desktop Cmd+Enter test now expects a send with one answer (the blank
question locks null), the compressor test expects the single-answer summary
the code gives, and locales/_keys.desktop.json is regenerated for the
removed and added clarify strings.
* test(tui): the one-question card header is singular
|
||
|
|
8cb4fdc925 |
fix(process): heartbeats wake the agent only on new output, and never as a user bubble
A `terminal(background=true, heartbeat=N)` tick queued a notification every N seconds
whether or not the process had printed anything, and every queued event costs the owning
session a full model turn. On Desktop and the TUI that turn painted the wake as a user
bubble ("[Background process ... heartbeat #9 ... (no new output since the last
heartbeat)]") followed by the model's "Still running normally." — over and over, for a
process whose row on the status stack already said it was running — and while the wake
held the session's turn, the user's own prompt sat queued behind it.
- `ProcessRegistry._emit_heartbeat` skips a tick with no new output. The sequence counts
delivered beats only; the "(no new output)" placeholder in the formatter is gone.
- TUI/Desktop type heartbeat rows `display_kind: hidden` (the kind both clients and the
transcript preview already honour); the CLI paints a one-line receipt and persists the
row hidden, so reopening the session in Desktop shows only the agent's reply.
- Desktop hydration drops heartbeat rows persisted by older backends the same way.
- `display.background_process_notifications: off` is honored by the TUI/Desktop poller and
the CLI drain, not just the messaging gateway. `off` mutes process-driven wakes only:
a finished `delegate_task(background=true)` still lands.
Supersedes #123123 (cherry-picked; scoped so `off` keeps subagent results) and #119202
(cherry-picked; `heartbeat: 0` is schema-valid so models that materialize every field
stop tripping the foreground guard).
|
||
|
|
bb3011a202 |
fix(video_gen): resolve OpenRouter and DeepInfra credentials per profile, not from os.environ
The OpenRouter video backend read OPENROUTER_API_KEY and OPENROUTER_BASE_URL straight from os.environ. That broke two setups: - A key added with `hermes auth add openrouter` (API key or OAuth) lives in the credential pool, not the environment. Chat and image_gen/openrouter find it through resolve_runtime_provider. video_gen reported OpenRouter unavailable, and generate() returned missing_credentials. - On a multiplexed gateway, os.environ holds the launch profile's .env. A routed profile's video jobs were submitted, polled and downloaded with the launch profile's key and billed to that account. A profile whose key lived only in its own .env could not use the backend at all. The backend now resolves (api_key, base_url) with resolve_runtime_provider(requested="openrouter"), the same call image_gen/openrouter makes. generate() resolves once and passes the pair to submit, poll and download. With a round-robin pool, resolving per request would poll with a different account's key than the one that created the job. OpenAICompatibleVideoGenProvider, which the DeepInfra video backend uses, had the same raw reads of <NAME>_API_KEY and <NAME>_BASE_URL. Both now go through get_secret_str, as image_gen/deepinfra already does. |
||
|
|
550d74c62f |
fix: manage_catalog documented under its own setup toolset; post-hook ownership contract exercises it
tools-reference.md listed manage_catalog under the connections section, which the reference-docs contract resolves against the connections toolset. The agent-runtime post-hook ownership test enumerates every tool in AGENT_RUNTIME_POST_HOOK_TOOL_NAMES; manage_catalog now runs through both executor paths there. |
||
|
|
31e59b9441 |
feat: setup agent can search the catalog and install plugins and skills through the approval card
The setup profile's `setup` toolset was empty. It now carries one tool, manage_catalog: - search: catalog plugins (the Plugins tab's live catalog resolver) and hub skills, with whether each is already installed in the default profile. Read-only. - install: opens the same connection operation manage_connections opens, with rows of kind plugin / skill. Nothing installs until the user approves a row. An approved row installs into `default` (or the profile the Advanced modal named) through dashboard_install_plugin / the hub's headless install, so the catalog pin, kill list, security scan and live activation (#119644) are the host's. The row settles with the live MCP tool names and the plugin's skill. The model sends catalog ids and an action only; every other key is refused before anything runs. An unknown id or a plugin this OS cannot run is drawn failed with the installer's own text. Anywhere a catalog card cannot be drawn (TUI, CLI, messaging, registry dispatch) the result is the `hermes plugins install` / `hermes skills install` pointer. - contract: plugin/skill targets follow the MCP transitions. - run.apply_answer / reissue route a card answer to the module that owns the operation. - tool_search: `setup` joins the direct-surface toolsets, so the guide's one tool is never deferred behind tool_search. - docs: tools reference, toolsets reference, plugin catalog page. Linear NS-964. |
||
|
|
e636aedebf | fix: preserve known file baselines across partial rereads | ||
|
|
65b0ff38ea |
feat(terminal): heartbeat notifications for long background processes
`terminal(background=true, heartbeat=N)` emits a `heartbeat` event every N seconds (floor 60) carrying only the output produced since the previous one, plus the usual completion notice. The agent stays current on a long bounded job (merge train, full test suite, deploy) and reacts to a failure within N seconds instead of at exit. Why: `notify=true` fires once at exit, so multi-hour merge trains ran silently — the parent's prompt cache went cold and every "what's up" cost 5–10 rediscovery calls; 30 days of batch sessions show 4,930 hand status polls (14% of all tool calls). `notify=[pattern]` cannot serve this: its lifetime cap (8) exists precisely to stop periodic output from flooding the agent. Mechanism: one daemon timer thread for every heartbeat session (reader threads block on the pipe and cannot keep time); delta output via a total-ingested counter over the rolling buffer; heartbeats only where the completion notice can be delivered (same async-support/subagent gates), never after exit. `heartbeat_seconds` is checkpointed. Rendered on CLI, gateway and TUI through the existing process-notification paths. |
||
|
|
afc3b7c6f3 |
feat(connectors): one backend-owned connection operation, with a setup card on Desktop, TUI and CLI (#111008)
* feat(connectors): the desktop connects apps through one backend-owned operation Re-based onto main after #109517, #110368, #110574 and #110843 landed as squash merges ( |
||
|
|
6ae4cb88d5 |
fix(docs): reference pages name tools the registry never registered
The shipped tool-surface references still document the pre-consolidation surface: tools-reference.md lists cronjob/todo/process/project_create/ project_list/project_switch/open_preview/close_preview/read_preview/tour/tip (6 uncallable, 5 hidden dispatch-only aliases), and toolsets-reference.md still claims web_search is a member of the browser toolset — membership |
||
|
|
19b29df13b |
fix: video generation tools no longer let the agent pick the model
video_generate advertised an optional `model` argument (and the xAI edit/extend tools a model override) so the LLM could route a single call to a different model family — a different endpoint and billing tier — than the one the user selected in `hermes tools`. image_generate never exposed this, and #83080 asked to extend it there; the ruling is the opposite: models do not choose models. The `model` property is gone from the static and dynamic video_generate schema and from xai_video_edit / xai_video_extend; a `model` smuggled into the call is ignored and the configured `video_gen.model` (then the provider default) is what reaches the request. Config-side selection (`video_gen.model`, `video_gen.<provider>.model`, `<PROVIDER>_VIDEO_MODEL`) is unchanged, and the xAI plugin's explicit-model branch is no longer reachable from the tool layer. Refs #83080 |
||
|
|
2fbcd8b0ea |
docs(website): link pages by relative Markdown path so they open on GitHub (#114428)
Mechanical `check_doc_links.py --fix` pass over website/docs (hand-authored and generated pages) and the zh-Hans mirror: 1,868 route-style links (`](/section/page#anchor)`, `](/docs/...)`) become `](../section/page.md#anchor)`. Every target was asserted to exist on disk; anchors and query strings are preserved; fenced code blocks and inline-code examples are untouched. Two dead targets found by the converter were fixed by hand first: memory-providers.md linked `/user-guide/plugins` (page is `user-guide/features/plugins`), and the zh-Hans learning-path still linked the removed `rl-training` page — ported the EN treatment (external Atropos link). Docusaurus build after: EN locale 0 unresolved Markdown links, 0 broken links, 0 broken anchors. |
||
|
|
98b4efbf48 |
fix: clarify cards that cannot render are re-asked as plain text, never reported as user inactivity
A native clarify card on a messaging platform (Telegram inline keyboard, Slack blocks) could fail without the user ever seeing a question, and the agent then waited out the full clarify_timeout and reported "[user did not respond within Nm]" (#112684): - the platform rejects the card at once -> the wait aborted with a delivery sentinel but the question was never re-asked; - the send outruns the 15 s acknowledgement window and only then fails (pool/connect timeouts, a stale-thread retry) -> the runner never looked at the send future again and blocked for the whole timeout for a card that never posted; - no status adapter at all -> _ask_clarify_question returned ('', False), so a batch reported timed_out=True with an empty notice. gateway/run_turn_runner_clarify_delivery.py (new topical sibling; the send-disposition helpers move out of the gateway/run.py facade) now retries every definitive card failure once through the adapter's plain-text send_clarify (numbered list + text capture) - except a connector egress DECLINE, where re-sending the text is the exfiltration the guard exists to stop - and watches a possibly-delivered send so a late failure releases the waiter with "[clarify prompt could not be delivered]". The no-surface case reports "[clarify prompt could not be delivered: no chat surface]" (same prefix every consumer already treats as a non-answer). Live probe (real TurnRunner + real clarify_gateway, Telegram-shaped adapter, timeout 30 s): card fails after 16 s -> before 45.2 s and "[user did not respond within 0m]", after 16.7 s and the typed answer to the text prompt; card rejected at once -> before sentinel with no text prompt, after the numbered prompt is sent and "2" resolves to the choice. |
||
|
|
b3aef45987 |
fix(clarify): batch result carries the surface's no-answer notice; trim tests
Follow-up to the #112725 salvage. When a gateway clarify batch stops at an unanswered question, the batch payload only said `timed_out: true` with blank answers — so a card the platform REJECTED ("[clarify prompt could not be delivered]") read exactly like a user who walked away, which is the misreport #112684 describes ("a timeout must not be presented as user inactivity when the prompt was never delivered"). - gateway/run_turn_runner.py::_clarify_batch_sync: the unanswered question's own response text rides along as `notice`. - tools/clarify_tool.py::_run_batch/_batch_result: pass `notice` through into the result JSON beside `timed_out` (only when present); schema description mentions it. CLI/TUI batch callbacks send no notice -> byte-identical output. - tests: fold the single-question re-arm pin into the existing bracket-answer test (<=2 new tests), parametrize the end-to-end test over an undeliverable Telegram-shaped adapter so the delivery sentinel is pinned as `notice`. - docs: tools-reference.md describes `notice`. Part of #112684 |
||
|
|
ac63d0eea5 |
fix(agent): execution guidance and browser hints drop the web_search stripper; tests assert the invariant
The rebased guidance text no longer names web_search anywhere, so
execution_guidance_text()'s replace() calls (
|
||
|
|
e819846b10 |
Inspired by Amp: relative time bounds (7d/24h/2w) + wrapper forwarding for session_search after/before
Amp's thread feed supports relative time filters (`after:7d`, `updated_before:7d`) alongside ISO dates. Extend the salvaged after/before bounds (PR #86067 by @Moodtuner997) the same way: - `_parse_iso_bound()` now accepts relative durations `Nh`/`Nd`/`Nw` (case-insensitive) meaning "now minus N", alongside ISO dates/datetimes. Clearer error message names both accepted forms. - Forward after/before/exclude_session_ids through the public `session_search()` wrapper (the PR predates the wrapper/impl split; without this the SQL bounds were unreachable from the registry handler — same class as the earlier `detail` forwarding fix). Appended after `detail` to preserve positional compatibility. - Tool schema descriptions teach both forms. - Tests: relative after/before against the discovery shape, unit checks for h/d/w math, case-insensitivity, and bad-unit rejection. - Docs: tools-reference row mentions time bounds + exclude_session_ids. |
||
|
|
4613f895ab |
test: give the rewrite-hint fixtures a read baseline; align docs row with schema
The stale-write guard now refuses write_file on an existing file the task never read in full, so test_write_file_rewrite_hint's overwrite-without-read fixtures were refused before the hint could be computed. Reading first is the exact read->whole-file-rewrite pattern the hint exists for. tools-reference.md's write_file row now mirrors the WRITE_FILE_SCHEMA description (one-sentence contract + the recovery step) instead of a longer paraphrase. |