12 Commits
Author SHA1 Message Date
Teknium 4ea8d23cc9 fix(agent): side-question review fixes (unattended turns, profile-bound leases, billing, attribution)
- No side_question_token for cron runs (platform="cron") or delegated subagent turns
  (_delegate_depth > 0): nobody reads the answer, so it is pure unattended spend.
- A lease is bound to the profile home it was issued in; a plugin context of another profile
  gets None and leaves the lease for its owner.
- Usage bills to the session the token was issued for (capture.session_id), not the parent's
  id at release time, which can rotate between issue and redeem.
- Plugin side questions report as aux_task="plugin_side_question" with the asking plugin_id in
  pre/post_auxiliary_call, and record usage as plugin_side_question:<plugin_id>; /btw keeps
  side_question.
- Drop the new legacy key llm.allow_side_question: the capability is declare + consent only.
- The timeout starts before the fork build, so runtime resolution counts against it.
2026-10-24 19:56:21 -07:00
Teknium 3f7af72898 fix(agent): side-question review findings (runtime pin, extra_body tools, hook bracket, lease cap)
- Refuse when the parent's model/provider/base_url/api_mode/reasoning
  changed since the turn was captured: a voice-chat turn routed to
  auxiliary.voice_chat restores the main runtime right after
  post_llm_call, and the captured prompt must not go out on another model.
- has_server_side_tools also reads extra_body.tools: request_overrides
  land there and the SDK merge sends them over the top-level tools.
- One pre/post_auxiliary_call bracket around the whole attempt, so a
  reply that fails after the provider returned (no choices) still closes
  the observer's request with its error; a cancelled call reports an
  InterruptedError instead of an empty success.
- Drop the 32-lease global eviction: it revoked live sessions' tokens on
  a busy gateway. The table already holds at most one lease per live
  agent (new turn, expiry and a dead parent all purge).
2026-10-24 19:56:21 -07:00
Teknium 4047c3ab72 fix(agent): ctx.side_question reports through pre/post_auxiliary_call
The side request fired the turn-scoped pre_api_request/post_api_request
after its turn had already closed, so trace-per-turn observers (and the
relay metrics keyed on them) counted a plugin's side call as a model call
of a finished turn. Every other side call (titling, compression, MoA,
vision, ...) reports through pre/post_auxiliary_call instead.

- turn_api_request: build_request_payload() is the payload build through
  the LLM request middleware with no hook; build_api_request() wraps it
  with pre_api_request, the debug dump and the MoA handshake, unchanged
  for the turn loop.
- auxiliary_hooks: start_aux_call_hooks()/finish_aux_call_hooks() expose
  the pre/post pair for callers that drive the provider call themselves;
  post accepts an already-normalized (message, finish_reason, usage) view
  for a transport-native reply.
- side_question_request: build_request_payload + aux hooks with
  aux_task="side_question"; a request refused for server-side tools now
  fires nothing.

Test: a real plugin observing all four events sees pre/post_api_request
for the turn only and pre/post_auxiliary_call (with usage) for the side
request (red before).
2026-10-24 19:56:21 -07:00
Cleo a8db83446f feat(plugins): ctx.side_question asks the session's own agent after a turn
post_llm_call now carries a single-use side_question_token when a loaded
plugin holds the new llm.side_question capability. ctx.side_question(token,
prompt) redeems it: ONE text-only request on a cache-parity fork through
the turn loop's own request phases (assemble_api_request, build_api_request,
perform_api_call), never the tool loop. The parent's system prompt, tools[],
reasoning config and max_tokens are replayed verbatim, so the request reads
the warm prompt cache. Tool calls in the reply are dropped, nothing is
persisted, and usage lands on the session as side_question.

Lease rules: one attempt per turn shared by every plugin (consumed even on
failure), 60 s, invalid once the parent starts its next turn, prompt capped
at 4096 chars, timeout capped at 60 s. The lease holds its parent weakly and
runs under the turn's captured contextvars (profile and secret scope).
Refused with None: MoA, agent-as-provider runtimes, requests that carry
provider-executed tools.

has_capability moves with side_question into a PluginContext mixin
(hermes_cli/plugins_capability_ctx.py) so hermes_cli/plugins.py shrinks.

Fixes #143788
2026-10-24 19:56:21 -07:00
TekniumandYuan Li 952322ca3c fix(plugins): inherited aux slots follow their base until the user pins one
Salvage follow-up for #129189 on top of #129209:

- An unknown or self-referential `inherit_from` now logs a warning and
  registers the task without inheritance instead of raising, so one bad
  base no longer fails the whole plugin load (the #129191 approach).
- `provider: auto` on an inheriting task means "no preference", like the
  empty strings: "Reset all to auto" (CLI + dashboard) writes exactly that,
  and before this it pinned the slot to the main provider while the model
  was still inherited from the base (mismatched route).
- Once the user pins a route on the task (non-auto provider, model or
  base_url), provider/model/base_url/api_key/api_mode/key_env/effort all
  come from the task's own block. Mixing per key let a pinned provider pick
  up the base's base_url and api_key.
- The CLI picker resolves the inherited view only for inheriting tasks.

Tests collapse to behavior contracts: base followed and changes
propagate, picker placeholders keep following, a pin wins whole, bad
bases warn and are ignored, re-registration cycles are cut.

Co-authored-by: Yuan Li <dskwelmcy@163.com>
2026-10-05 17:24:40 -07:00
thanhan-a17andTranquil-Flow 56afae600e fix(dashboard,desktop): surface plugin-registered auxiliary tasks in Models settings
`PluginContext.register_auxiliary_task` already lists a plugin task in the `hermes model`
picker and layers its defaults into `auxiliary.<key>`, but every REST/UI surface behind the
Models page enumerated only the built-in `_AUX_TASK_SLOTS`:

- `GET /api/model/auxiliary` never listed the task
- `POST /api/model/set` (scope=auxiliary) rejected it: `400 unknown auxiliary task`
- `__reset__` and the empty-task broadcast skipped it; `_stale_aux_pins` ignored it
- Desktop Settings → Models and the dashboard ModelsPage hardcoded the built-in list

So a plugin's side model could only be pinned by hand-editing config.yaml per profile.

Fix, at the existing seams:
- `web_server_config._aux_task_slots()` = built-ins + plugin keys from
  `get_plugin_auxiliary_tasks()`, fail-soft; every consumer in that module uses it.
- `GET /api/model/auxiliary` reads plugin tasks INSIDE `_profile_scope`, so a request for
  profile B enumerates B's plugins (the plugin manager keys on the same context-local home) —
  the profile-scope gap flagged on #40922. Built-in rows are byte-identical; plugin rows are
  appended with `label`/`hint`/`plugin` from the registration.
- Desktop `model-settings.tsx` and dashboard `ModelsPage.tsx` render served plugin rows after
  the built-ins using the server label/hint (pure `auxTaskRows`; no new i18n strings, no-op on
  older backends).

Tests: real plugin loaded from a temp HERMES_HOME through PluginManager discovery, two
profiles in one process (listing scoped per profile; foreign task still 400), assignment /
broadcast / reset / stale-pin over a plugin slot, discovery failure leaves built-ins intact —
all red on main. Vitest covers the pure row merge and the rendered plugin row.

Fixes #40880. Salvages #40922.

Co-authored-by: Tranquil-Flow <tranquil_flow@protonmail.com>
2026-10-05 17:24:40 -07:00
Apostol Apostolov be3f0fcc1c feat(plugins): let auxiliary tasks inherit a base slot at read time
register_auxiliary_task's defaults start from a fixed shape and every key
is a full replace, so a plugin whose side model is a near-copy of a
built-in has to restate the whole block and silently drifts when that
built-in's shape changes.

inherit_from names a built-in or already-registered task whose effective
config becomes the base, resolved at read time so the plugin tracks the
base's current values instead of snapshotting them. Precedence is base,
then plugin defaults, then user config.

Self-referential inherit_from is rejected at registration rather than
left to the read-time guard: same-owner re-registration is allowed, so
the bad value would otherwise surface as unbounded recursion in a config
read. The guard stays for a hand-edited registry.

Closes #129189
2026-10-05 17:24:40 -07:00
teknium1 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.
2026-09-18 14:27:04 -07:00
Magnus Hedemark 79c11aea7a fix(plugins): complete task-routed LLM integration 2026-08-12 16:25:20 -07:00
Teknium 43874d1a96 docs: accuracy sweep + coverage for 2 months of shipped features
Accuracy pass (all 373 pages audited against code, 13 parallel audits):
- configuration.md: 12 stale defaults/keys (file-sync rewrite, clarify
  timeout, streaming knobs, iteration budget, TTS/STT enums)
- reference/: commands/env-vars/toolsets/tools synced with
  COMMAND_REGISTRY, argparse tree, OPTIONAL_ENV_VARS, TOOLSETS
  (28 env vars added, 3 phantom removed, mcp__ naming, webhook
  platform restricted toolset)
- features/, messaging/, developer-guide/, guides/: ~60 factual fixes
  (web_extract truncation, dashboard auth fail-closed, delegation
  blocked tools, adapter signatures, session schema v23, phantom
  Matrix env vars, hermes setup tts, auth spotify, webhook --skills)
- zh-Hans: explicit heading IDs fix 2 broken WSL2 anchors

New coverage for features shipped in the last 2 months (verified
against code before writing):
- compression.in_place, verify-on-stop (+v31/v32 migration reality),
  ${env:VAR} SecretRef, display.timestamp_format, session:compress
  hook + thread_id/chat_type fields
- /journey learning timeline, per-channel model/system-prompt
  overrides, /sessions search, clarify multi-select, -z --usage-file,
  uninstall --dry-run, config get/unset
- MCP elicitation, extra_headers, discover_models, api-server run cap,
  Bedrock cachePoint, Discord reasoning_style, Google Chat clarify
  cards, vibe reactions, resume cwd restore, Yuanbao forwarded
  messages, api_content sidecar, roaming pet, tool_progress log mode,
  WhatsApp polls/locations, kanban per-task model + lifecycle hooks
2026-07-29 08:48:05 -07:00
Teknium 1d5deac346 fix(website): cross-locale doc links + drop empty ko locale (#31895)
The locale switcher appeared broken because hardcoded markdown links
(`](/docs/X)`) got double-prefixed by Docusaurus to `/docs/<locale>/docs/X`
(404) in non-English locales, and the MDX hero `<a href>` on the index
page escaped locale routing entirely.

Changes:
- Rewrite 922 `](/docs/X)` -> `](/X)` across 166 docs files (strip trailing
  .md too). Docusaurus prepends locale + baseUrl itself.
- docs/index.md -> index.mdx; hero "Get Started" anchor -> Docusaurus
  <Link> so it stays inside the active locale.
- Drop `ko` locale entirely from docusaurus.config.ts + delete i18n/ko/
  (4 stale auto-translated kanban pages, <2% coverage, misleading).

Verified `npm run build` succeeds for both en and zh-Hans; `build/zh-Hans/
index.html` has no /docs/zh-Hans/docs/... double-prefixed paths.

PR2 will translate the 335 English docs into i18n/zh-Hans/.
2026-05-24 23:16:20 -07:00
Teknium 5aa755e4e6 feat(plugins): run any LLM call from inside a plugin via ctx.llm (#23194)
* feat(plugins): host-owned LLM access via ctx.llm

Plugins can now ask the host to run a one-shot chat or structured
completion against the user's active model and auth, without ever
seeing an OAuth token or API key. Closes the gap where plugins that
needed bounded structured inference (receipts, CRM extraction,
support classification) had to either bring their own provider keys
or register a tool the agent had to call.

New surface on PluginContext:
- ctx.llm.complete(messages, ...)
- ctx.llm.complete_structured(instructions, input, json_schema, ...)
- async siblings ctx.llm.acomplete / acomplete_structured

Backed by the existing auxiliary_client.call_llm pipeline — every
provider, fallback chain, vision routing, and timeout policy Hermes
already supports applies automatically.

Trust gate (fail-closed by default):
- plugins.entries.<id>.llm.allow_model_override
- plugins.entries.<id>.llm.allowed_models (allowlist; '*' = any)
- plugins.entries.<id>.llm.allow_agent_id_override
- plugins.entries.<id>.llm.allow_profile_override

Embedded model@profile shorthand goes through the same gate as
explicit profile=, so it can't bypass the auth-profile policy.
Conflicting explicit and embedded profiles fail closed.

Also lands:
- plugins/plugin-llm-example/ — reference plugin that registers
  /receipt-extract, demonstrating image+text structured input,
  jsonschema validation, and the trust-gate config.
- website/docs/developer-guide/plugin-llm-access.md — full API docs.
- 45 unit tests covering trust gates, JSON parsing, schema
  validation, image encoding, async surface, and config loading.

Validation:
- 2628 tests pass in tests/agent/
- E2E: bundled plugin loaded with isolated HERMES_HOME, slash
  command produced parsed JSON via stubbed call_llm
- response_format extra_body wired correctly for both json_object
  and json_schema modes

* docs(plugin-llm): rewrite quickstart and framing

The quickstart now uses a meeting-notes-to-tasks example instead of
a receipt extractor, and the page leads with hook-time / gateway
pre-filter / scheduled-job framing rather than the OpenClaw
KB/support/CRM/finance/migration enumeration that the original
upstream PR used. Receipt example moved to a separate worked
example link so the docs page itself doesn't echo any of the
upstream framing.

Also clarifies where ctx.llm fits in the broader plugin surface
(table comparing register_tool / register_platform / register_hook
/ etc.) and what makes this lane different from auxiliary_client
internals.

No code change.

* docs(plugin-llm): reframe as any LLM call, not just structured output

The original draft leaned heavily on complete_structured() and made
the chat lane (complete() / acomplete()) feel like a footnote.
Restructure so:

- The page title and description say 'any LLM call.'
- The lead shows BOTH a plain chat call (error rewriter) AND a
  structured call (triage scorer) up top.
- Quick start has two complete plugin examples — /tldr (chat) and
  /paste-to-tasks (structured).
- New 'When to use which' table for choosing complete() vs
  complete_structured() vs the async siblings.
- Trust-gate sections explicitly note 'all four methods,' and the
  request-shaping list calls out chat-only fields (messages) and
  structured-only fields (instructions, input, json_schema)
  alongside each other.
- The 'Where this fits' section now says 'for any reason,
  structured or not.'

The receipt-extractor reference plugin still exists under
plugins/plugin-llm-example/ — but the docs page no longer treats
it as the canonical surface example. It's now described as 'a third
worked example, this time with image input.'

No code change.

* feat(plugin-llm): split provider/model into independent explicit kwargs

The first cut accepted a single 'provider/model' slug on every method
and split it internally. That looked clean but broke under live test:
the model-override path tried to use the slug's vendor prefix as a
literal Hermes provider id, which silently switched the user off
their aggregator (e.g. plugin asks for 'openai/gpt-4o-mini' on a user
who routes through OpenRouter — host attempted to call the 'openai'
provider directly, failed because OPENAI_API_KEY wasn't set).

New shape mirrors the host's main config:

  ctx.llm.complete(
      messages=[...],
      provider='openrouter',         # gated, optional
      model='openai/gpt-4o-mini',    # gated, optional
      profile='work',                # gated, optional
      ...
  )

Each is independently gated by its own allow_*_override flag.
Granting model-override does NOT auto-grant provider-override.
Allowlists are now per-axis (allowed_providers, allowed_models)
matched literally against whatever string the plugin sends.

Dropped 'model@profile' embedded-suffix shorthand entirely. Hermes
doesn't use that pattern anywhere else; profile= is its own kwarg.

Live E2E (against real OpenRouter via Teknium's config) confirms:
- zero-config call works
- default-deny blocks each override with a helpful error
- model-only override stays on user's active provider (the bug)
- provider+model override switches cleanly
- allowlist refuses non-listed entries
- structured output round-trip parses + schema-validates

Tests: 49 cases (up from 45); all green. Docs updated to match the
new shape, including a 'most plugins never need this section' callout
on the trust-gate config block.

* fix+cleanup(plugin-llm): real attribution, hook-mode coverage, move example out of core

Three integration fixes for the ctx.llm surface:

1. Attribution bug — result.provider and result.model now reflect
   what call_llm actually used, not placeholder fallbacks ('auto',
   'default'). New _resolve_attribution() helper:

     - explicit overrides win (what the call targeted)
     - response.model wins for the recorded model (provider
       canonicalisation: 'gpt-4o' → 'gpt-4o-2024-08-06' etc.)
     - falls back to _read_main_provider() / _read_main_model()
       when no override is set, so audit logs reflect the user's
       active main provider/model
     - 'auto' / 'default' only when EVERYTHING is empty

   Live verified: zero-config call now records
   provider='openrouter', model='anthropic/claude-4.7-opus-20260416'
   instead of provider='auto', model='default'.

2. Hook-mode coverage — TestHookMode confirms ctx.llm.complete
   works from inside a registered post_tool_call callback. The
   docs page promised hook integration; now there's a test that
   exercises the lazy-import path through the real invoke_hook
   machinery. Two cases: traceback-rewrite hook with conditional
   ctx.llm.complete, and minimal hook regression for the
   sync-hook + sync-llm path.

3. Reference plugin moved out of core. plugins/plugin-llm-example/
   is gone from hermes-agent — it now lives in the new
   NousResearch/hermes-example-plugins companion repo. The docs
   page links there. Hermes' bundled plugins should be plugins
   users actually run; reference / docs-companion plugins live
   externally.

Test count: 56 (up from 49). Wider sweep on tests/hermes_cli/
+ tests/gateway/ + tests/tools/ + tests/agent/ shows 16770
passing; the 12 failures are all pre-existing on origin/main
(verified by stashing this branch's changes and re-running) —
kanban-boards, delegate-task, gateway-restart, tts-routing —
none touch the plugin_llm surface.

* chore(plugins): move all example plugins to companion repo

Reference / docs-companion plugins now live exclusively in
NousResearch/hermes-example-plugins, not bundled with the core repo:

- example-dashboard
- strike-freedom-cockpit

A new fourth example, plugin-llm-async-example, was added to that
repo demonstrating ctx.llm's async surface (acomplete()) with
asyncio.gather() — registers /translate <lang>: <text> which fires
forward translation + sentiment classifier in parallel, then a
back-translation for QA. Live-tested at 2.5s for three real
provider round-trips (would be ~5-6s sequential).

Docs updated:
- developer-guide/plugin-llm-access.md links both sync and async
  examples in the Reference section
- user-guide/features/extending-the-dashboard.md repoints both demo
  sections to the companion repo with corrected install paths
- user-guide/features/built-in-plugins.md drops the two demo rows
- AGENTS.md notes that example plugins live in the companion repo

Net: hermes-agent's plugins/ directory now contains only plugins
users actually run (memory providers, dashboard tabs that ship real
features, the disk-cleanup hook, platform adapters). All four
demo / reference plugins live externally where they can be cloned
on demand instead of inflating the core install.
2026-05-10 07:09:28 -07:00