Skip to content

feat(hermes): first-class Omnigent model control for the hermes harness - #1971

Open
appletechie wants to merge 2 commits into
omnigent-ai:mainfrom
appletechie:polly/hermes-control
Open

feat(hermes): first-class Omnigent model control for the hermes harness#1971
appletechie wants to merge 2 commits into
omnigent-ai:mainfrom
appletechie:polly/hermes-control

Conversation

@appletechie

@appletechie appletechie commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Gives Omnigent first-class model control over the hermes harness. Before this, a model chosen for a hermes agent (args.model, executor.model, or a per-session /model override) was silently dropped: _build_spawn_env_from_spec returned None for hermes and the sys_session_send dispatch gate rejected any model ("no model-override plumbing"). The HermesExecutor already threaded HARNESS_HERMES_MODELhermes chat -m <model>; the only missing link was Omnigent feeding a model to it.

What changed

  • hermes (CLI-subprocess, OWN_AUTH): registered hermes → HARNESS_HERMES_MODEL in model_env_keys (harness_plugins.py) — the single source that drives the SDK-override set, the env-key map, and the dispatch gate. Added _build_hermes_spawn_env (runtime/workflow.py, mirroring the kimi sibling: model + cwd + os_env only, no provider/profile surface since it's OWN_AUTH) and wired the hermes branch into _build_spawn_env_from_spec.
  • hermes-native (resident TUI, native harness): threads the override as a top-level hermes -m <model> argv at terminal launch (runner/app.py _hermes_launch_args_with_model), guarded against a user-pinned -m/--model/--model= in passthrough args. Intentionally kept out of model_env_keys (like cursor-native).
  • Bugfix: _read_model_from_hermes_config (hermes_native_forwarder.py) assumed model was a string, but real Hermes configs store {default, provider} — so the web model/usage readout was blank. Now surfaces model.default, preserves the legacy string shape, and stays defensive.

Verification

  • Live, against the installed hermes CLI: hermes chat -v -m gpt-5.4"AI Agent initialized with model: gpt-5.4" vs the gpt-5.5 config default; HARNESS_HERMES_MODEL=gpt-5.4 drives HermesExecutor(model=gpt-5.4)hermes chat … -m gpt-5.4.
  • Tests: spawn-env threading + override application, model_env_keys/override-gate registration, native -m launch guard, dict-shaped config reader. Full local gate green (442 passed); harness-enumeration invariant (test_configured_harness_map_covers_all_spellings) included and passing.
  • Independent cross-vendor review: 0 blocking / 0 non-blocking, merge-ready.

Demo

hermes model switch

Real gpt-5.5 → gpt-5.4 override across the init line, conversation-turn log, and the actual API call (Hermes CLI v0.18.0). See the PR comment for the transcript.

@github-actions github-actions Bot added the size/L Pull request size: L label Jul 4, 2026
@appletechie
appletechie marked this pull request as ready for review July 4, 2026 23:54
@github-actions
github-actions Bot requested a review from dhruv0811 July 4, 2026 23:54
@appletechie

Copy link
Copy Markdown
Contributor Author

Demo — hermes model override, end-to-end (gpt-5.5 → gpt-5.4)

hermes model switch

Two real headless runs (Hermes CLI v0.18.0, hermes chat -q "say hi" -Q -v, provider openai-codex):

  • Default (config gpt-5.5): AI Agent initialized with model: gpt-5.5 → API call model=gpt-5.5
  • Override (-m gpt-5.4): AI Agent initialized with model: gpt-5.4 → API call model=gpt-5.4

The -m override flips the init line and the conversation-turn log and the actual API call — this is exactly the path args.model / executor.model / a per-session /model now drive. No credentials in the image.

@github-actions

Copy link
Copy Markdown
Contributor

Reviewer SLA — this PR has been awaiting review from @dhruv0811 for more than 5 working days. Adding @TomeHirata as a second reviewer.

@dhruv0811

Copy link
Copy Markdown
Contributor

/review

@omnigent-ci

omnigent-ci Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Polly AI Review

Summary

Solid, tightly-scoped change that closes a real gap: models chosen for the hermes harness were previously dropped because hermes was absent from model_env_keys and _build_spawn_env_from_spec returned None for it. The three surfaces — SDK-subprocess spawn-env (HARNESS_HERMES_MODEL), native-TUI -m launch argv, and the config-reader bugfix — are each wired consistently and well-tested. I traced the full chain and found no blocking correctness or security issues. Recommend merge after confirming the one native-launch flag note below.

Blocking issues

None. Verified:

  • model_env_keys() now maps hermes → HARNESS_HERMES_MODEL, which is the single source feeding both _SDK_MODEL_OVERRIDE_HARNESSES (via frozenset(model_env_keys())) and the _HARNESS_MODEL_ENV_KEY dict in app.py, so the SDK-override set, env-key map, and dispatch gate stay in sync automatically.
  • _build_hermes_spawn_env correctly mirrors the kimi sibling (model + cwd + os_env only) and the executor consumes HARNESS_HERMES_MODEL as hermes chat … -m <model>. The _build_spawn_env_from_spec branch passes cwd=cwd and the shared model_override post-step overwrites the baked model as intended.
  • _hermes_launch_args_with_model guards against a user-pinned -m/--model/--model= before appending, matching the cursor-native pattern; launch_config.model_override is validated via validate_model_override upstream in _pi_native_launch_config, so no unsanitized value reaches the argv.
  • The _read_model_from_hermes_config dict-shape handling preserves the legacy string branch and stays defensive (isinstance guards, best-effort except).

Security vulnerabilities

None. The model override flows through validate_model_override (strict charset regex anchored to an alphanumeric first char, preventing flag-injection like --model --evil) before ever reaching either the spawn-env var or the -m argv. No secrets, no new network surface, no deserialization risk (yaml.safe_load).

Non-blocking notes

  • Native launch flag form unverified for bare hermes. The executor path forwards the model to the chat subcommand (hermes chat … -m <model>), and the PR's live verification demonstrates exactly that (hermes chat -v -m gpt-5.4). The native TUI path, however, appends -m as a top-level argv to the bare hermes command (no chat subcommand) — hermes -m <model>. That relies on -m being accepted as a global/default-subcommand flag, which the cited verification (all hermes chat …) does not directly cover. Worth a quick confirmation against the installed CLI that bare hermes -m <model> is honored (and doesn't error out) before relying on the resident-TUI path.
  • -m vs fork --resume ordering. When a fork clones history, --resume <id> is appended to hermes_args and then _hermes_launch_args_with_model appends -m after it. Assuming -m and --resume are both accepted global flags this is fine, but it's another argument for confirming the top-level flag grammar above.

Automated review by Polly · workflow run

@TomeHirata TomeHirata removed their assignment Jul 14, 2026
@TomeHirata
TomeHirata removed their request for review July 14, 2026 02:09
@appletechie
appletechie force-pushed the polly/hermes-control branch from 2891fb9 to 9b78af7 Compare July 23, 2026 09:54
@appletechie

Copy link
Copy Markdown
Contributor Author

Sorry for the slow turnaround on the review note — confirming both points now, verified directly against an installed Hermes CLI (v0.19.0, 2026.7.20, upstream 67e592b0).

1. -m is a genuine top-level flag on bare hermes (no chat subcommand), so the native-TUI path's argv is valid:

usage: hermes [-h] [--version] [-z PROMPT] [--usage-file PATH] [-m MODEL]
              [--provider PROVIDER] [-t TOOLSETS] [--resume SESSION]
              [--no-restore-cwd] [--continue [SESSION_NAME]] [--worktree]
              [--accept-hooks] [--skills SKILLS] [--yolo] [--pass-session-id]
              [--ignore-user-config] [--ignore-rules] [--safe-mode] [--tui]
              [--cli] [--dev]
              {chat,model,moa,fallback,secrets,egress,migrate,gateway,proxy,lsp,setup,...}

options:
  -m, --model MODEL     Model override for this invocation

2. -m vs --resume ordering is a non-issue — both are top-level options in the same usage line (--resume, -r SESSION Resume a previous session by ID or title), so argparse accepts them in any order. Appending -m after --resume in the fork path is fine.

Worth noting this is v0.19.0 while the original demo was v0.18.0, so the flag survived a minor version bump rather than being a point-in-time observation.

Rebasing onto current main now to clear the conflict.

dokploy-bot and others added 2 commits July 29, 2026 11:48
… harness

Register the `hermes` CLI-subprocess harness in `model_env_keys`
(`HARNESS_HERMES_MODEL`) and add `_build_hermes_spawn_env` so `args.model` /
`executor.model` — and a per-session `/model` override — actually reach the
Hermes CLI as `hermes chat -m <model>`. Before this, `_build_spawn_env_from_spec`
returned `None` for hermes and the `sys_session_send` dispatch gate rejected any
model ("no model-override plumbing"), so a model chosen for a hermes agent was
silently dropped. The executor already consumed `HARNESS_HERMES_MODEL` and
threaded `-m`; the only missing link was the registration + spawn-env builder.

hermes-native (the resident TUI) is a native harness, so — like cursor-native —
the override reaches the CLI as a top-level `hermes -m <model>` argv at terminal
launch (new `_hermes_launch_args_with_model` helper, guarded against a
user-pinned `-m`/`--model` in passthrough args), not via an env var. It is
therefore intentionally kept out of `model_env_keys`.

Also fix `_read_model_from_hermes_config`: real Hermes configs store `model` as
a `{default, provider}` mapping, so the old `isinstance(str)` check always
returned None and the web usage/model readout was blank; now surface
`model.default`.

Mechanism verified end-to-end against the installed hermes CLI:
`hermes chat -v -m gpt-5.4` reports `AI Agent initialized with model: gpt-5.4`
vs the `gpt-5.5` default, and `HARNESS_HERMES_MODEL=gpt-5.4` drives the harness
wrap -> `HermesExecutor(model=gpt-5.4)` -> `hermes chat ... -m gpt-5.4`.

Tests: spawn-env threading + override application, model_env_keys/override-gate
registration, native `-m` launch guard, and the dict-shaped model reader. Full
local gate green (442 passed): harness readiness/plugins/capabilities invariants,
run-harness-without-agent e2e, runtime/harnesses, model_override, and all hermes
suites; ruff check + format clean.

Co-authored-by: omnigent <noreply@omnigent.ai>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: apeltekci <andrew@peltekci.com>
…itch)

Terminal-rendered PNG of two REAL headless `hermes chat -q "say hi" -Q -v`
turns captured under a PTY against Hermes Agent CLI v0.18.0 (provider
openai-codex). Default run initializes on the ~/.hermes/config.yaml default
gpt-5.5; adding `-m gpt-5.4` flips the AI-agent init line, the conversation
turn, and API call #1 to gpt-5.4 — showing Omnigent's model override reaching
the hermes CLI. No credentials in the asset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: apeltekci <andrew@peltekci.com>
@appletechie
appletechie force-pushed the polly/hermes-control branch from 9b78af7 to 6283349 Compare July 29, 2026 18:52
@appletechie

Copy link
Copy Markdown
Contributor Author

Rebased onto current main — conflict cleared, 0 commits behind.

One thing worth flagging, since the diff moved files and that isn't obvious from a rebase:

The native-TUI half had to be re-ported. _hermes_launch_args_with_model and its call site lived in runner/app.py, which #3495 / #3500 extracted into runner/native/orchestration.py. Taking the new base wholesale silently dropped that half — the branch would still have looked green (the spawn-env path and its tests are in different files), but the resident TUI would have stopped honouring /model. Both now live next to _auto_create_hermes_terminal, and the three unit tests import from the new module.

Two incidental cleanups the rebase surfaced:

  • hermes now takes effective_spec in _build_spawn_env_from_spec, matching every sibling after refactor(harness): route native spawn-env through the provider seam (PR 1.5a) #3495 — the original branch passed the bare spec, which predates effective_spec existing.
  • Dropped a now-dead _HARNESS_MODEL_ENV_KEY literal entry. Upstream added _HARNESS_MODEL_ENV_KEY = model_env_keys() immediately below the dict, so the literal is overwritten on the next line; the harness_plugins.py registry entry is the real (and only) source. app.py is down from +43 to +3 as a result.

Re-verified after the rebase: pytest tests/test_hermes_native.py tests/test_hermes_native_forwarder.py tests/runtime/test_hermes_spawn_env.py tests/test_model_override.py194 passed. pre-commit run ruff-check --all-files / ruff-format --all-files clean.

Wider sweep (tests/runner/ tests/runtime/ tests/test_harness_plugins.py) is 2440 passed / 16 failed, and all 16 reproduce identically on a clean upstream/main worktree — test_uc_function (4, missing Databricks SDK), test_hindsight_local_dispatch (3, no API key), codex-native terminal-registry tests (7), and test_provider_spawn_env codex credential tests (2). None touch the hermes paths.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-sla-escalated size/L Pull request size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants