Commit Graph

21 Commits

Author SHA1 Message Date
Brooklyn Nicholson
a4c31d592b fix(tools): reject all-blank and over-limit clarify choices instead of dropping them silently
A choices list whose entries were all blank quietly became an open-ended
question, so the model never learned its options were gone and Desktop
showed a free-text card for a choice prompt (#73152). A choice longer
than a surface renders was dropped downstream while choices_offered still
reported it as shown (#124127).

_normalize_questions now returns a tool error for both: a non-empty
choices list with no surviving label names the question, and a choice
over MAX_CHOICE_CHARS (8000, also declared as maxLength in the schema)
names its position and the limit. An omitted or empty choices list stays
open-ended, which is still the documented way to ask free-text.

Co-authored-by: Konstantin Khlopkov <47825603+kokhlo@users.noreply.github.com>
Co-authored-by: JonthanaHanh <92574114+JonthanaHanh@users.noreply.github.com>
2026-09-29 19:44:05 -05:00
Siddharth Balyan
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
2026-09-29 18:33:44 +05:30
teknium1
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
2026-09-16 17:47:50 -07:00
Teknium
e83816a4d1 review-fix(comments): restore lost #NNNN rationale comments across non-test source (mechanical sweep, condensed, code unchanged)
For each issue anchor present in BASE 63279301bc non-test .py and absent on HEAD, the BASE comment/docstring block was re-attached at the HEAD location of the code it explained (matched by the distinctive code line / enclosing def). Sentences already covered by an existing HEAD comment were deduped; the issue number always survives. Insert-only: no code lines changed.
2026-09-03 09:44:26 -07:00
Teknium
4d1c644668 refactor(tools): simplify clarify tool/gateway — unify answer cleaning, JSON parsing, label matching; compact docs 2026-09-02 22:59:41 -07:00
Teknium
731a943ac4 refactor(tools/planning): compact kanban metadata merging and clarify tool/gateway helpers 2026-09-02 14:47:02 -07:00
Teknium
d4cec15b47 refactor(tools): first-wave simplification of tools/ (file ops split, lazy_deps, code_exec, approval, browser, delegate, mcp, skills, terminal, voice, media)
Behavior-neutral structural pass over tools/*: god-file extractions into
sibling modules (file_operations_common/lint/search, file_tools_paths/
read_tracking/write, code_execution_env/rpc, tool_search_catalog/names/
validation, tts_command_provider, ...), duplicate helper unification,
if/elif -> dispatch tables, dead-code removal, docstring compaction.
Tool schemas (get_tool_definitions) verified byte-identical to base.
2026-09-02 14:43:45 -07:00
Teknium
7d6c6ae4ae refactor(clarify): schema diet + single questions[] interface (880 → 335 tok/call, −62%) (#95907)
* refactor(clarify): halve the schema (880->436 tok/call) — same rules, half the words

* refactor(clarify): one question field — questions[] is the only advertised shape (single = one-entry array; legacy shape stays handler-accepted)

* refactor(clarify): unadvertise per-question id — response rows already carry question text + order
2026-08-26 16:42:44 -07:00
ethernet
66bb77cbf9 docs(clarify): advertise the questions batch in the tool description
The `questions` parameter had a full description, but the top-level
tool description still described three single-question modes and never
mentioned batching. The model decides how to call a tool from that
description, so it kept asking one question per call.

The description now states that 2-5 independent questions can go in
one call and that one batched call is preferred over a chain of
single-question calls. The parameter description also tells the model
to put a short batch title in the still-required top-level question.
Two schema tests pin the contract: the description names the batch
capability, and the questions parameter stays optional with the
MAX_QUESTIONS cap.
2026-08-18 21:28:53 -04:00
ethernet
bd8b658a63 feat(clarify): accept a questions batch in the clarify tool core
The clarify tool gets an optional questions parameter (2-5 independent
questions, issue #18450). Batch-capable platform callbacks receive the
normalized list in one call and reply with per-question answers. Legacy
callbacks are looped one question at a time. The loop stops on timeout
so the user is not asked the remaining questions after they walk away.
Locked answers survive a timeout: the result carries them plus a
timed_out flag, and unanswered entries have an empty user_response.

The single-question path is byte-identical to the previous behavior.
2026-08-18 21:28:53 -04:00
Brooklyn Nicholson
10cf651484 feat(clarify): label the agent's recommended choice on every surface
The clarify schema now tells the model to order choices best-first, and
mark_recommended tags element 0 with "(Recommended)" at the tool layer --
the one platform-agnostic entry point -- so CLI, TUI, desktop, and every
messaging adapter inherit the label without a copy each. Each surface
already defaults its cursor to index 0, so the recommendation is the
pre-highlighted row too.

The label is presentation only: strip_recommended takes it back off
user_response, and choices_offered reports the bare list, so the agent
never reasons about (or echoes back) a string it did not write. Typed
replies on messaging platforms match with or without the suffix.
2026-08-13 01:19:42 -05:00
teknium1
1a7f73b8ea refactor: migrate hand-rolled error envelopes to shared tool_error()
Replace json.dumps({"error": ...}) boilerplate with the documented
tools/registry.py tool_error() helper across 13 files.

Migrated: 59 sites (58 code sites + 1 docstring example in
path_security.py), incl. multi-key envelopes passed via kwargs
(available_actions, path/already_read, pattern/already_searched,
parameters/hint, needs_reauth/server, error_type/tool/result_type).
Also removed 2 now-redundant local tool_error imports in mcp_tool.py
in favor of a module-level import.

Skipped (not byte/shape-compatible with tool_error):
- {"success": false, "error": ...} envelopes (browser_tool,
  browser_camofox, browser_dialog_tool, web_tools, tts_tool,
  skills_tool, image_generation_tool, project_tools, memory_tool,
  cronjob_tools, x_search_tool, xai_video_tools) — leading keys
  differ; key order would change.
- terminal_tool/code_execution_tool envelopes carrying output/
  exit_code/status leading keys.
- tool_search.py:912-area multi-key success paths (non-error).
- mcp_tool.py MCPSampling._error — returns MCP-spec ErrorData
  object, not a JSON string; incompatible.
- send_message_tool._error — returns a dict (not str) and applies
  secret redaction; return type must be preserved.

Behavior note: sites that previously omitted ensure_ascii=False now
emit raw UTF-8 (tool_error's canonical behavior) — JSON-equivalent.

Tests: 23 targeted files (tool_search, discord, file_tools/read
guards/operations, registry, clarify, homeassistant, code_execution,
send_message, delegate, terminal, mcp, model_tools, sanitize_tool_error,
retaindb plugin) — all pass. ruff clean.
2026-07-29 10:14:00 -07:00
teknium1
0b670f0539 fix(clarify): route multi_select through current dispatch paths and harden callback detection
Follow-ups to the salvaged #23768 commit, which targeted a pre-79559214
codebase:

- agent/tool_executor.py + agent/agent_runtime_helpers.py: pass
  multi_select at both current clarify dispatch points (the PR's
  run_agent.py edits landed on dead code paths).
- tools/clarify_tool.py: replace the broad TypeError-retry in
  _invoke_callback with inspect.signature detection, so a compatible
  callback that raises TypeError internally is not invoked twice
  (addresses hermes-sweeper review feedback on #23768).
- tests: cover single-invocation on internal TypeError, legacy 2-arg
  callbacks, **kwargs callbacks, and registry handler multi_select
  pass-through (schema arg → handler → callback).
2026-07-26 17:46:55 -07:00
Ghislain LE MEUR
3e2f91f6b3 feat(clarify): add multi-select (checkbox) support to clarify tool
Adds a `multi_select` boolean parameter enabling checkbox-style
multi-choice questions (Space to toggle, Enter to confirm).
Backward compatible — defaults to single-select when omitted.

- tools/clarify_tool.py: schema + handler + _parse_multi_select_response
- run_agent.py: both dispatch points pass multi_select
- cli.py: checkbox UI, key bindings, rendering, edge cases
- hermes_cli/callbacks.py: TUI fallback callback
- hermes_cli/oneshot.py: oneshot multi-select message
- tests/tools/test_clarify_tool.py: 12 new tests (35 total)
2026-07-26 17:46:55 -07:00
alt-glitch
9e1f616136 fix(clarify): docstring — put options in choices[] only, never enumerate in question text
The model was enumerating options inside the question string (dead prose the UI
can't render as pickable rows). Schema description now spells out: choices[] is
REQUIRED for selectable options; question holds ONLY the question.
2026-06-19 07:34:02 -07:00
teknium1
2c3aebcadc fix(clarify): unwrap dict choices at the source so every surface gets clean text
The Discord fix (previous commit) handles dict-shaped clarify choices at the
Discord adapter only. The same dict-repr leak originates upstream at
tools/clarify_tool.py's str(c).strip() normalization — the single
platform-agnostic point both the CLI and every gateway adapter flow through.

When an LLM emits [{"description": "..."}] instead of bare strings, str(c)
produced {'description': '...'} which leaked onto the CLI panel
(cli.py:13048/13081), was returned verbatim as the user's answer
(cli.py:11945), and hit Telegram's numbered list too.

Add _flatten_choice (same label->description->text->title unwrap as the
Discord adapter, name/value excluded, keyless dicts dropped) and apply it at
the normalization line. Fixes CLI + Telegram + all platforms at the root;
the Discord smart-truncation now operates on already-clean text.

Adds johnjacobkenny to AUTHOR_MAP for the salvaged commit.
2026-06-19 06:31:08 -07:00
Teknium
678a87c477 refactor: add tool_error/tool_result helpers + read_raw_config, migrate 129 callsites
Add three reusable helpers to eliminate pervasive boilerplate:

tools/registry.py — tool_error() and tool_result():
  Every tool handler returns JSON strings. The pattern
  json.dumps({"error": msg}, ensure_ascii=False) appeared 106 times,
  and json.dumps({"success": False, "error": msg}, ...) another 23.
  Now: tool_error(msg) or tool_error(msg, success=False).

  tool_result() handles arbitrary result dicts:
  tool_result(success=True, data=payload) or tool_result(some_dict).

hermes_cli/config.py — read_raw_config():
  Lightweight YAML reader that returns the raw config dict without
  load_config()'s deep-merge + migration overhead. Available for
  callsites that just need a single config value.

Migration (129 callsites across 32 files):
- tools/: browser_camofox (18), file_tools (10), homeassistant (8),
  web_tools (7), skill_manager (7), cronjob (11), code_execution (4),
  delegate (5), send_message (4), tts (4), memory (7), session_search (3),
  mcp (2), clarify (2), skills_tool (3), todo (1), vision (1),
  browser (1), process_registry (2), image_gen (1)
- plugins/memory/: honcho (9), supermemory (9), hindsight (8),
  holographic (7), openviking (7), mem0 (7), byterover (6), retaindb (2)
- agent/: memory_manager (2), builtin_memory_provider (1)
2026-04-07 13:36:38 -07:00
Teknium
8bb1d15da4 chore: remove ~100 unused imports across 55 files (#3016)
Automated cleanup via pyflakes + autoflake with manual review.

Changes:
- Removed unused stdlib imports (os, sys, json, pathlib.Path, etc.)
- Removed unused typing imports (List, Dict, Any, Optional, Tuple, Set, etc.)
- Removed unused internal imports (hermes_cli.auth, hermes_cli.config, etc.)
- Fixed cli.py: removed 8 shadowed banner imports (imported from hermes_cli.banner
  then immediately redefined locally — only build_welcome_banner is actually used)
- Added noqa comments to imports that appear unused but serve a purpose:
  - Re-exports (gateway/session.py SessionResetPolicy, tools/terminal_tool.py
    is_interrupted/_interrupt_event)
  - SDK presence checks in try/except (daytona, fal_client, discord)
  - Test mock targets (auxiliary_client.py Path, mcp_config.py get_hermes_home)

Zero behavioral changes. Full test suite passes (6162/6162, 2 pre-existing
streaming test failures unrelated to this change).
2026-03-25 15:02:03 -07:00
teknium1
210d5ade1e feat(tools): centralize tool emoji metadata in registry + skin integration
- Add 'emoji' field to ToolEntry and 'get_emoji()' to ToolRegistry
- Add emoji= to all 50+ registry.register() calls across tool files
- Add get_tool_emoji() helper in agent/display.py with 3-tier resolution:
  skin override → registry default → hardcoded fallback
- Replace hardcoded emoji maps in run_agent.py, delegate_tool.py, and
  gateway/run.py with centralized get_tool_emoji() calls
- Add 'tool_emojis' field to SkinConfig so skins can override per-tool
  emojis (e.g. ares skin could use swords instead of wrenches)
- Add 11 tests (5 registry emoji, 6 display/skin integration)
- Update AGENTS.md skin docs table

Based on the approach from PR #1061 by ForgingAlex (emoji centralization
in registry). This salvage fixes several issues from the original:
- Does NOT split the cronjob tool (which would crash on missing schemas)
- Does NOT change image_generate toolset/requires_env/is_async
- Does NOT delete existing tests
- Completes the centralization (gateway/run.py was missed)
- Hooks into the skin system for full customizability
2026-03-15 20:21:21 -07:00
teknium1
08ff1c1aa8 More major refactor/tech debt removal! 2026-02-21 20:22:33 -08:00
teknium1
9350e26e68 feat: introduce clarifying questions tool for interactive user engagement
- Added a new `clarify_tool` to enable the agent to ask structured multiple-choice or open-ended questions to users.
- Implemented callback functionality for user interaction, allowing the platform to handle UI presentation.
- Updated the CLI and agent to support clarify questions, including timeout handling and response management.
- Enhanced toolset definitions and requirements to include the clarify tool, ensuring availability across platforms.
2026-02-19 20:06:14 -08:00