Skip to content

feat(plugins): transform_api_error_classification hook so provider plugins own their error quirks - #58524

Closed
webdevtodayjason wants to merge 5 commits into
NousResearch:mainfrom
webdevtodayjason:feat/plugin-error-classifier-hook
Closed

feat(plugins): transform_api_error_classification hook so provider plugins own their error quirks#58524
webdevtodayjason wants to merge 5 commits into
NousResearch:mainfrom
webdevtodayjason:feat/plugin-error-classifier-hook

Conversation

@webdevtodayjason

@webdevtodayjason webdevtodayjason commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

What

Responding to the lead dev's call for plugin-interface expansion ideas — this is the first concrete seam, shipped as code instead of a suggestion: a transform_api_error_classification plugin hook, consulted at the top of classify_api_error() before the built-in pipeline, so model-provider plugins can own their provider's error-classification quirks without core patches.

  • New "transform_api_error_classification" entry in VALID_HOOKS (contract documented inline, same style as pre_verify / pre_gateway_dispatch). Callbacks receive the parsed error context (provider, model, status_code, error_type, error_code, error_message, error_body, error, approx_tokens, context_length, num_messages), self-scope on provider, and return None to pass or {"reason": "<FailoverReason name>", ...optional recovery-hint overrides}.
  • get_plugin_error_classification() mirrors get_pre_tool_call_block_message(): first valid result wins, invalid dicts/unknown reasons are skipped, exceptions are isolated — a broken plugin can never break classification.
  • Zero behavior change when no plugin claims the error. All 179 existing tests in tests/agent/test_error_classifier.py pass untouched.

Why this seam

A large slice of the open-PR backlog is single-provider error-classification patches: #58451, #58355, and the tool-use-404 piece of #58502, plus the class they represent. Each one is an if provider == X-shaped edit to agent/error_classifier.py waiting on review. (Two earlier citations were struck after review feedback: #58366 is chain-build-time model resolution and #58474 is credential-pool resolution; different layers, not absorbed by this hook.) With this hook, that entire contribution class becomes plugin territory — publishable today, no merge required.

Review updates (July 15)

Per the sweeper review: rebased onto current main, where the OpenRouter tool-use 404 is now handled natively, so the bundled reference plugin (whose whole point was that gap) is removed per the standalone-repo policy for vendor integrations. All hook tests now use a synthetic unclaimed error (fake provider, neutral message, no status code) that no present or future built-in rule can claim, so the fixtures cannot go stale the way the OpenRouter one did.

The hook is also now explicitly Python-plugin-only: VALID_HOOKS doubles as the shell-hook allow-list, and the shell response parser has no channel for the classification directive, so a shell registration is refused at config parse with a warning (new SHELL_UNSUPPORTED_HOOKS set) instead of being silently ignored. The hook is documented in the hooks reference as the third behavior-changing hook with its full kwargs contract.

Review updates (August 12)

Per the #64231 batch-disposition verdict (SALVAGE): the hook id is renamed to transform_api_error_classification, the taxonomy transform-family name, across VALID_HOOKS, the dispatch helper, the shell-hook exclusion, tests, and the hooks.md catalog. Chaining semantics are now explicit per the #64714 rule: run-all-then-pick-first, first valid result in registration order wins, and every valid-but-losing result logs a runtime warning instead of being silently shadowed. Rebased onto current main (715d26c).

Testing

  • 14 tests in tests/test_transform_api_error_classification_hook.py: baseline fall-through, plugin-wins, override-of-builtin, enum/string reasons, dataclass-default hints, invalid-reason/non-dict skipping, first-valid-wins, skipped-result runtime warning, helper-exception isolation, hook kwargs contract, message/error_context sanitization, and a synthetic plugin self-scoping and running end-to-end through the real invoke_hook path (no mocks).
  • tests/agent/test_error_classifier.py: 75 passed, untouched by this branch.
  • Manual end-to-end with a real PluginManager discovery + plugins.enabled config: plugin loads, hook registers, the 404 classifies as model_not_found, and unclaimed errors (e.g. 429 → rate_limit) are untouched.
  • ruff check clean on all touched files.

Taxonomy classification (for the #64231 batch review)

🤖 Generated with Claude Code

https://claude.ai/code/session_01FWMcB7RPSYUpsXDfBgwjzM

@alt-glitch alt-glitch added type/feature New feature or request comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have labels Jul 4, 2026
@Kolomaster68

Copy link
Copy Markdown

Nice seam — provider plugins owning their own error quirks makes a lot of sense, and it should genuinely shrink that backlog class. 👍

One small correction to the list though: #58366 (mine) isn't an error-classification patch — it doesn't touch agent/error_classifier.py. It adds a recommended:free sentinel to fallback_providers so the chain tracks the Portal's rotating free model (model resolution at chain-build time, not error handling at request time). Different code path, so it wouldn't be absorbed by this hook — just flagging so it doesn't get swept up in a batch cleanup if this lands.

@webdevtodayjason

Copy link
Copy Markdown
Contributor Author

Good flag, you're right. #58366 is chain-build-time model resolution, not request-time error classification. Different layer, wouldn't be absorbed by this hook. Pulled it from the PR description so it doesn't end up in anyone's batch-cleanup list.

Took a second look at the rest of the citations while I was in there and struck #58474 too. That one is credential-pool resolution, also not classifier territory. The solid citations for this class are #58451, #58355, and the tool-use-404 piece of #58502. Description is updated.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for proposing a concrete plugin seam and isolating malformed callback results.

Problems

  • The bundled OpenRouter example is now stale. Current main already matches the exact phrase in agent/error_classifier.py:295 and returns model_not_found, retryable=False, and should_fallback=True in the 404 path at agent/error_classifier.py:929-934 (commit 2bb11adb4976169f2434ce72a6185cb189d83079). Consequently, the new baseline assertion in tests/test_classify_api_error_hook.py:48 is no longer true. Please remove or replace that provider-specific demo; third-party integrations also belong in standalone plugin repos under the repository policy.
  • VALID_HOOKS is also the shell-hook allow-list (agent/shell_hooks.py:311-336). But _parse_response() at agent/shell_hooks.py:566-620 does not preserve {\"reason\": ...} directives for this event, so a configured shell hook can register successfully while its classification is ignored. Support the shell response shape with tests, or make this Python-plugin-only.
  • Please document this behavior-changing hook in website/docs/user-guide/features/hooks.md; its current reference says only pre_tool_call and pre_llm_call affect behavior (:374-397).

Suggested changes

  • Retain generic hook tests using a synthetic unclaimed provider error, and add the shell-path or explicit exclusion plus public contract documentation.

Automated hermes-sweeper review.

Comment thread tests/test_classify_api_error_hook.py Outdated
monkeypatch.setattr(plugins_mod, "_plugin_manager", plugins_mod.PluginManager())

result = _classify_tool_use_404()
# Documents the gap the demo plugin closes: a generic 404 with no

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This baseline is stale on current main: commit 2bb11adb4976169f2434ce72a6185cb189d83079 added this exact phrase to _MODEL_NOT_FOUND_PATTERNS, and the 404 path now returns model_not_found with retryable=False and fallback. Replace the demo premise with an unclaimed synthetic provider error.

Comment thread hermes_cli/plugins.py Outdated
# First valid result (registration order) wins. Invalid dicts and
# unknown reasons are skipped; exceptions are isolated — a broken
# plugin can never break error classification.
"classify_api_error",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this to VALID_HOOKS also enables hooks.classify_api_error shell hooks, but agent/shell_hooks.py:_parse_response() only preserves pre-tool, pre-verify, or context responses. A shell hook returning the documented {reason: ...} directive is silently ignored; either implement that response path with tests or keep this event out of the shell-hook allow-list.

@webdevtodayjason
webdevtodayjason force-pushed the feat/plugin-error-classifier-hook branch from c66959b to 5a2a0e7 Compare July 15, 2026 18:06
@webdevtodayjason

Copy link
Copy Markdown
Contributor Author

All three addressed in 5a2a0e759 (rebased onto current main).

  1. Stale demo: you're right, main now claims that exact 404 natively, which is the best possible outcome for the original bug. The bundled plugin is removed per the standalone-repo policy, and every hook test now uses a synthetic unclaimed error (fake provider, neutral message, no status code). I verified empirically that a status-less neutral message is the only shape guaranteed to reach the unknown/retryable fall-through on current main (any 4xx is claimed as format_error), so these fixtures cannot go stale the way the OpenRouter one did.

  2. Shell-hook gap: real catch. Added SHELL_UNSUPPORTED_HOOKS in hermes_cli/plugins.py containing classify_api_error, checked in _parse_hooks_block before the VALID_HOOKS gate: a shell registration for this event is now refused at config parse with a warning naming it Python-plugin-only, instead of registering and having its directive silently dropped by _parse_response. Regression test included; a future shell response shape can lift the event out of the set.

  3. Docs: classify_api_error is documented in website/docs/user-guide/features/hooks.md as the third behavior-changing hook, with the full kwargs contract, return shape, first-valid-wins semantics, and the Python-only note; the general-rules line and quick-reference table are updated to match.

Suites: tests/test_classify_api_error_hook.py 15 passed, tests/agent/test_shell_hooks.py and tests/agent/test_error_classifier.py green, 263 total across the three touched files.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jul 15, 2026
@webdevtodayjason
webdevtodayjason force-pushed the feat/plugin-error-classifier-hook branch from 5a2a0e7 to 16447f3 Compare July 30, 2026 14:51
@edwardgem

Copy link
Copy Markdown

The dispatch wording landed in #75861 (docs/plugins/hook-taxonomy.md) with your correction folded in — first-valid-wins, run-all dispatch, isolated failures, classify_api_error noted explicitly as run-all-then-pick-first.

Since we don't have push access to this branch, want to grab the conforming pass yourself now that you can see the exact wording, or would you rather I open a PR against your fork's branch (or drop a suggested diff here) for you to pull in? Either works — just don't want to duplicate effort if you're already planning to touch it.

@webdevtodayjason

Copy link
Copy Markdown
Contributor Author

Grabbed it myself, thanks for the pointer. 774329fac aligns all three spots (the VALID_HOOKS comment, the helper docstring, and the hooks.md section) to run-all-then-pick-first, adds the Privacy flag on error_message and error_body, and states the cold-path trigger explicitly. Wording only, no behavior change, 33/33 tests still green. One note: I didn't hard-link docs/plugins/hook-taxonomy.md since #75861 hasn't merged yet. Happy to add the link once it lands.

@alt-glitch alt-glitch added provider/openrouter OpenRouter aggregator needs-decision Awaiting maintainer decision before any implementation sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data and removed sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data provider/openrouter OpenRouter aggregator labels Aug 2, 2026
@alt-glitch alt-glitch removed the sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data label Aug 2, 2026
webdevtodayjason added a commit to webdevtodayjason/hermes-agent that referenced this pull request Aug 3, 2026
The plugin system's authoring contract currently lives only in code
comments (VALID_HOOKS block, PluginContext docstrings, middleware.py).
With the plugin-interface expansion underway, third-party authors need a
single reference. This adds docs/plugins.md covering:

- Quick start (manifest + register(ctx), enable, verify)
- All four discovery sources, name-collision precedence, path-derived keys
- plugin.yaml field reference and the five plugin kinds with their
  kind/source-aware loading rules (auto-load, deferred platform, opt-in)
- plugins.enabled/disabled/entries config semantics
- Full PluginContext surface: tools (incl. the allow_tool_override trust
  gate), slash + CLI commands, all 23 hooks with per-hook return
  contracts, the 4 middleware kinds with chaining/next_call semantics,
  backend provider registries and their config selectors, platforms,
  Slack actions, auxiliary tasks, skills, ctx.llm/inject_message/
  dispatch_tool/profile_name
- State & concurrency (plugins/plugin_utils primitives)
- Distribution via `hermes plugins install` (all URL forms,
  after-install.md, manifest_version)
- Debugging (HERMES_PLUGINS_DEBUG) and common gotchas
- "Converting a waiting PR into a plugin" mapping table

Every claim was cross-checked against hermes_cli/plugins.py,
hermes_cli/middleware.py, tools/registry.py, and shipped plugins
(spotify, google_meet, disk-cleanup); hook/middleware/method names in the
doc are verified to exist by script. The classify_api_error entry is
marked pending PR NousResearch#58524.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWMcB7RPSYUpsXDfBgwjzM
@webdevtodayjason

Copy link
Copy Markdown
Contributor Author

I'll grab the conforming pass myself, thanks for checking rather than duplicating. Since the code already does run-all-then-pick-first, this should mostly be docstring work: the Privacy: line on error_body and error_message, the cold-path note, and pointing the hook's VALID_HOOKS entry at hook-taxonomy.md. I'll flag it on #75861 if any of the wording doesn't survive contact with the code.

@webdevtodayjason

Copy link
Copy Markdown
Contributor Author

Applied the #64231 verdict conditions in 8a24479a4. The hook id is now transform_api_error_classification everywhere: VALID_HOOKS, the dispatch helper, the shell-hook exclusion, tests, and both hooks.md spots. Dispatch stays run-all-then-pick-first with the first valid result in registration order winning, and per the #64714 skipped-transform rule a valid-but-losing classification now logs a runtime warning instead of being silently shadowed, with a test covering both the warning and the no-conflict quiet path. Rebased onto current main (715d26c). 14/14 on the hook suite, 134/134 across the hook, shell-hook, and error-classifier suites, ruff clean. If the #64714 chaining resolution lands with different transform semantics I'll conform to it.

@webdevtodayjason webdevtodayjason changed the title feat(plugins): classify_api_error hook — provider plugins own their error quirks feat(plugins): transform_api_error_classification hook so provider plugins own their error quirks Aug 13, 2026
AnalogHubris pushed a commit to AnalogHubris/hermes-agent that referenced this pull request Aug 13, 2026
…t failed

TUI/desktop error frames often showed a generic "request failed" while the
classified detail (provider, model, base_url, HTTP status, failure_reason,
fallback) only reached agent.log. Add _classify_turn_error_message and use
it in _fail_inflight_turn, message.complete error payloads, and compute_host
turn.error frames.

Composes with NousResearch#56720 turn_failed and NousResearch#58524 classify_api_error (NousResearch#64182 item 3).
Observer-side framing only — no delivery-path mutation.
webdevtodayjason and others added 5 commits August 13, 2026 04:27
…n error quirks

Adds a plugin seam at the top of agent/error_classifier.classify_api_error()
(step 0, before the built-in pipeline) so model-provider plugins can classify
their provider's error quirks without patching core:

- New "classify_api_error" entry in VALID_HOOKS. Callbacks receive the parsed
  error context (provider, model, status_code, error_type, error_code,
  error_message, error_body, error, approx_tokens, context_length,
  num_messages), self-scope on `provider`, and return None to pass or a dict
  {"reason": "<FailoverReason name>", ...optional recovery-hint overrides}.
- get_plugin_error_classification() helper mirrors
  get_pre_tool_call_block_message(): first valid result wins, invalid dicts
  and unknown reasons are skipped, callback exceptions are isolated — a
  broken plugin can never break classification. Zero behavior change when no
  plugin claims the error (all 179 existing classifier tests pass untouched).
- Bundled reference plugin `openrouter-tool-use-404` (opt-in, like all
  bundled standalone plugins) re-implements PR NousResearch#58451: OpenRouter's
  "No endpoints found that support tool use" 404 carries no
  _MODEL_NOT_FOUND_PATTERNS signal, so it classifies as unknown/retryable
  and the retry loop burns 3-5 attempts on a deterministic rejection.
  The plugin classifies it as model_not_found (retryable=False,
  should_fallback=True) so the fast-fallback path fires immediately —
  demonstrating a waiting core PR converted to a publishable plugin.

Motivation: ~10 open PRs are single-provider error-classification patches
(NousResearch#58451, NousResearch#58355, NousResearch#58502, NousResearch#58474, NousResearch#58366, ...). This hook turns that whole
class of contribution into plugin territory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWMcB7RPSYUpsXDfBgwjzM
…review

Rebased onto current main, where the OpenRouter tool-use 404 is now
handled natively (the bundled demo's exact reason to exist), so the demo
plugin is removed per the standalone-repo policy and every test now uses
a synthetic unclaimed error (fake provider, neutral message, no status
code) that no present or future built-in rule can claim.

classify_api_error is now explicitly Python-plugin-only: VALID_HOOKS
doubles as the shell-hook allow-list, but the shell response parser has
no channel for the classification directive, so shell registrations are
refused at config parse with a warning instead of being silently
ignored (new SHELL_UNSUPPORTED_HOOKS set + regression test).

The hook is documented in the hooks reference as the third
behavior-changing hook, with the full kwargs contract, return shape,
and the Python-only note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnMCvi2vXqfs996AjVeF2F
…ating-hook taxonomy

The taxonomy write-up for NousResearch#64231 names the Shape B contract
run-all-then-pick-first: every registered callback runs with failures
isolated, then the first valid result in registration order wins. Align
the hook comment, helper docstring, and hooks.md section with that
wording, add the Privacy flag on error_message/error_body, and state the
cold-path trigger explicitly. Wording only, no behavior change.
The dispatch semantics, privacy note, and cold-path property were already
documented at the VALID_HOOKS entry and the dispatch helper; this adds the
explicit reference to the first-valid-wins shape in
docs/plugins/hook-taxonomy.md (landing via NousResearch#75861) and the cold-path note
on the helper docstring, per the contract review on NousResearch#64231.
…ousResearch#64231 verdict

Applies the batch-disposition SALVAGE conditions from NousResearch#64231: the hook id
moves to the taxonomy transform-family name, and run-all-then-pick-first
dispatch now logs a runtime warning when a valid-but-losing classification
is skipped (the NousResearch#64714 skipped-transform rule). Chaining semantics are
stated explicitly at the VALID_HOOKS entry, the dispatch helper docstring,
and the hooks.md catalog row and detail section.
@teknium1

Copy link
Copy Markdown
Contributor

Merged via #85231 (rebase-merge, your commits preserved as author) with a trim commit on top per review: hook test suite reduced to core coverage and hooks.md section condensed. All #64231 verdict conditions retained. Thanks @webdevtodayjason!

@teknium1 teknium1 closed this Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/plugins Plugin system and bundled plugins needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants