Skip to content

feat(agent): per-model system-prompt prelude (operation mode override) - #48101

Open
arminanton wants to merge 1 commit into
NousResearch:mainfrom
arminanton:feat/per-model-system-prompt-prelude
Open

feat(agent): per-model system-prompt prelude (operation mode override)#48101
arminanton wants to merge 1 commit into
NousResearch:mainfrom
arminanton:feat/per-model-system-prompt-prelude

Conversation

@arminanton

@arminanton arminanton commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds a system-prompt override mechanism, the kind coding-agent CLIs (Claude Code and others) expose via --system-prompt-file / --append-system-prompt-file. It lets an operator program a model to behave or operate to a specific standard at the system level: one or more verbatim Markdown files are injected as the first system content, ahead of Hermes' own identity/tools/memory layers, resolved per model via a glob map.

Because the resolved text is plain system content, each provider routes it to its own system channel with no special-casing (Anthropic system=, OpenAI messages[0] {role:"system"}, Gemini systemInstruction). It's disabled by default and entirely fail-soft (any missing file / malformed config yields an empty prelude and never breaks prompt build).

Why

Today the built-in guidance blocks are hardcoded; there's no clean way to hand a model a full, model-appropriate operating prompt without forking. This is useful when you want a specific model family to operate to a known standard. For example, you can run an Opus-class model to a named operating standard (e.g. operate as F@bl3 5) by pointing a *opus* rule at that prompt file — the model adopts the standard while Hermes' own layers still ride on top.

When is the prelude (re)loaded?

The system prompt is built once per session and cached, so the upstream prefix cache stays warm across turns. The prelude re-resolves whenever that cached prompt is rebuilt:

  • new session — built from scratch on the first turn (conversation_loop.py build path);
  • model switch mid-sessionswitch_model nulls agent._cached_system_prompt (agent_runtime_helpers.py:1656), so the next turn rebuilds and re-resolves to the new model's prelude;
  • context compression — the compressor rebuilds the prompt (conversation_compression.py:507).

A continuing session reuses the previous turn's prompt verbatim (to keep the cache prefix matching) until one of the above fires.

Changes Made

  • agent/system_prompt_prelude.py (new) — fail-soft resolver. Reads a system_prompt_prelude config block (or a standalone YAML via HERMES_PRELUDE_CONFIG), matches the runtime model id against fnmatch globs (both the full provider/model and the bare tail, case-insensitive), and stacks the matched files verbatim. First-match or layered (first_match: false). Returns a PreludeResolution carrying the text + provenance.
  • agent/system_prompt.py — resolve the prelude at the top of build_system_prompt_parts, expose it as a new prelude key, and join it ahead of stable/context/volatile in build_system_prompt.
  • hermes_cli/prompt_size.py — report the prelude tier in the prompt-size breakdown.
  • cli-config.yaml.example — documented (commented-out) system_prompt_prelude block including the optional operating-mode marker and its toggles.
  • tests/agent/test_system_prompt_prelude.py (new) — 19 tests: single match, stacking order, most-specific-first, bare-tail + case-insensitive matching, no-match/disabled/missing-file/all-missing fail-soft, layered mode, dedupe, absolute paths, empty/None model, and the operating-mode marker (prepended when set, profile alias, absent, custom template, empty-disables).

Optional operating-mode marker

A rule may name an operating_mode (e.g. operating_mode: "House"). When set, a short marker is prepended ahead of the prelude files that (a) names the active mode so the model can transparently report it ("running as House"), and (b) tells the model to treat the mode as authoritative over later conflicting framing in the prompt. That second part is what makes a loaded operating prompt actually stick instead of being second-guessed by the trailing Model:/Provider: metadata line. It's built from two framing tags aligned models weight as authoritative system content — <policy_spec> (the non-negotiable rule) and <system-reminder> (descriptive system info). It is off unless a rule names a mode, the template is overridable via the top-level operating_mode_marker config key, and setting that key to "" disables the marker entirely while keeping the mode name on the resolution. The marker is deliberately non-contradictory: it never tells the model to deny its underlying model, only to operate to the named standard.

Relation to prior work

Complementary to #44610 (agent.prompt_overrides): that reshapes Hermes' existing stable-tier fragments (replace/append/prepend/remove a named block); this prepends a new per-model operator tier ahead of everything. Different mechanism, different use case — they compose cleanly.

Type of Change

  • ✨ New feature (non-breaking change that adds functionality)

How to Test

  1. Create ~/.hermes/system-prompts/opus-operating.md with some operating text.
  2. In config.yaml:
    system_prompt_prelude:
      enabled: true
      base_dir: "~/.hermes/system-prompts"
      rules:
        - match: "*opus*"
          files: ["opus-operating.md"]
  3. Run an Opus model and inspect the prompt (hermes prompt-size shows the new prelude tier; HERMES_DUMP_REQUESTS=1 shows the prelude leading the system content).
  4. With enabled: false (default) or no matching rule, the prompt is byte-identical to before.
pytest tests/agent/test_system_prompt_prelude.py -q

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (feat(agent):)
  • I searched for existing PRs to make sure this isn't a duplicate (found feat(agent): declarative per-fragment system-prompt overrides #44610 — the complementary stable-tier-override approach; this is a distinct per-model prelude tier)
  • My PR contains only changes related to this feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass (19 new + the existing tests/agent/test_system_prompt.py + test_system_prompt_restore.py, no regression)
  • I've added tests for my changes
  • I've tested on my platform: Ubuntu Linux (Python 3.11)

Documentation & Housekeeping

  • I've updated relevant documentation (module docstring + the documented config block) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — done (new system_prompt_prelude block)
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows, macOS) — N/A (pure Python, utf-8 reads with explicit encoding; passes the Windows-footgun check)
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

ℹ️ Apply-time note (operator)

This PR cherry-picks CLEAN onto v0.17.0 (2bd1977d8), both standalone and after the other system-prompt-touching PR (#49917). A "conflict" seen in one bulk-stack experiment was a transient working-tree state from an earlier PR's conflict resolution in the same run, NOT a real conflict in this PR. Apply order: after #49917 is fine; either order resolves cleanly.

Adds a system-prompt override mechanism (like the --system-prompt-file flags
coding-agent CLIs expose): inject verbatim Markdown files as the FIRST system
content, ahead of Hermes' own identity/tools/memory layers, resolved per model
via a glob map. The resolved text is plain system content, so each provider
routes it to its own system channel (Anthropic system=, OpenAI messages[0],
Gemini systemInstruction). Disabled by default; fully fail-soft.

Re-resolves whenever the cached system prompt is rebuilt: new session, model
switch mid-session (switch_model nulls the cache, agent_runtime_helpers.py:1656),
or context compression. Continuing sessions reuse the prior prompt verbatim to
keep the prefix cache warm.

Optional operating-mode marker (off unless a rule names operating_mode) prepends
a short <policy_spec>+<system-reminder> block naming the mode and telling the
model to treat it as authoritative over later conflicting framing, so a loaded
operating prompt sticks. Overridable via operating_mode_marker (set to "" to
disable). Non-contradictory: never tells the model to deny its underlying model.

Complementary to NousResearch#44610 (which reshapes existing stable-tier fragments); this
prepends a new per-model tier ahead of everything. 19 tests; documented config
block in cli-config.yaml.example.
@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/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have labels Jun 17, 2026
@arminanton arminanton changed the title feat(agent): per-model system-prompt prelude (operator override) feat(agent): per-model system-prompt prelude (operation mode override) Jun 18, 2026
@arminanton
arminanton marked this pull request as ready for review June 18, 2026 18:05
arminanton added a commit to arminanton/hermes-agent that referenced this pull request Jun 21, 2026
…file-residual analysis

- external_cherrypick_all_prs.sh: real git cherry-pick of all 40 feature PR REMOTE heads
  onto fresh v0.17.0 = 37 CLEAN + 2 documented drifts (NousResearch#48069, NousResearch#50056); NousResearch#48101 conflict is
  a transient (clean in isolation).
- INDEPENDENT-VERIFICATION-ROUND3.md: confirms NousResearch#50033 remote head carries the utf-8 fix;
  explains all 94 whole-file diffs (upstream drift / PR-adds-new-file / private-deferred);
  per-file content-containment vs remote heads = 0 uncovered.
arminanton added a commit to arminanton/hermes-agent that referenced this pull request Jun 21, 2026
…usResearch#4)

Full stack on v0.17.0: build OK (0 compile errors), test slice 672 passed / 0 failed
vs src baseline 604 passed / 0 failed (delta = PR-added new tests; both 100% pass, no
regressions). Cherry-pick 37 clean + 3 documented-drift-resolved. Operator apply-time
conflict notes added to NousResearch#48069/NousResearch#50056/NousResearch#48101 descriptions.
arminanton added a commit to arminanton/hermes-agent that referenced this pull request Jun 21, 2026
- OVERLAP-DISJOINTNESS-TABLE.txt: 14/14 overlap files have pairwise-disjoint hunks
  (empirical clean sequential apply onto v0.17.0). Corrects a v0.16-coordinate false-positive.
- fresh_clone_repro.sh + .out: fresh fork clone, cherry-pick 40 PRs onto v0.17.0, diff vs src.
  5 apparent-residuals all classified (4 deferred-by-design in NousResearch#50111 + 1 NousResearch#48101 bulk-stack
  artifact, correct standalone); 0 real residual.
- pristine-baseline-COMMAND.txt: exact repro command + output; NousResearch#50066/NousResearch#50086 failure sets
  byte-identical to pristine v0.17.0 (comm -23 empty).
- NousResearch#50111 confirmed isolated (0 importable src), not required for src re-application.
arminanton added a commit to arminanton/hermes-agent that referenced this pull request Jun 22, 2026
…olution semantic review

Council: (3) per-hunk justification — every unmapped hunk blamed via git log -S to its
origin commit, mapped to a standing user instruction (exclusion) OR a shipped PR. 216/216
accounted, 0 uncovered. Found+resolved 11 initially-uncovered (all in shipped test-cluster
PRs NousResearch#48065/NousResearch#48101/NousResearch#49644/NousResearch#50032/NousResearch#50080/NousResearch#50078). (4) semantic-equivalence review of the
resolution patches: all 6 active ones re-anchor their PR's exact intent onto v0.17.0, no
silent behavior change; removed the DEAD agent_gemini_cloudcode_adapter patch (never
invoked — withdrawn file — and imported withdrawn google_user_agent).

@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 the focused, cache-aware implementation. The feature premise remains valid: current agent/system_prompt.py:497-520 exposes and joins only stable, context, and volatile; there is no equivalent per-model leading prelude on main.

Problems

  • agent/system_prompt_prelude.py:232 ignores the provider argument accepted at line 214, although the caller supplies it at agent/system_prompt.py:101-103. Provider-qualified glob rules therefore cannot work when agent.model is bare.
  • agent/system_prompt_prelude.py:228-230 hardcodes ~/.hermes; use profile-aware get_hermes_home() instead. The canonical resolver is platform-aware at hermes_constants.py:46-77.
  • The new configuration is absent from hermes_cli/config.py:976 DEFAULT_CONFIG, and HERMES_PRELUDE_CONFIG at agent/system_prompt_prelude.py:143 adds a non-secret configuration environment override contrary to AGENTS.md:62-64.
  • website/docs/developer-guide/prompt-assembly.md:29-35 still documents three tiers, while this PR adds a fourth.

Suggested changes

  • Match both provider/model and bare model IDs; add an end-to-end temp-HERMES_HOME test through prompt assembly.
  • Derive the default directory from get_hermes_home(), add it to DEFAULT_CONFIG, remove the environment override, and update the prompt-assembly documentation.

Automated hermes-sweeper review.

str(cfg.get("base_dir") or "~/.hermes/system-prompts").strip()
)
first_match = cfg.get("first_match", True)
ids = _candidate_ids(model)

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.

provider is accepted by resolve_prelude() and passed from the prompt builder, but this candidate list ignores it. A rule such as anthropic/* cannot match when agent.model is claude-opus-4-6; construct a provider/model candidate as well as the bare-model candidate and add coverage for that runtime shape.

return PreludeResolution("", [], None)

base_dir = os.path.expanduser(
str(cfg.get("base_dir") or "~/.hermes/system-prompts").strip()

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 default hardcodes the default-profile POSIX path. It bypasses HERMES_HOME for named profiles and is incorrect on native Windows, where get_hermes_home() resolves under LOCALAPPDATA. Derive the implicit directory from get_hermes_home() / "system-prompts" instead.

# Env override lets a test or a sandboxed run point at a different config
# without editing config.yaml. Value is a path to a YAML file with the same
# top-level ``system_prompt_prelude`` block.
override = (os.getenv("HERMES_PRELUDE_CONFIG") or "").strip()

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.

Please remove this user-facing non-secret configuration environment override. The project configuration policy requires behavioral settings to live in config.yaml; use a temp HERMES_HOME and config fixture for tests instead.

@teknium1 teknium1 added sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jul 14, 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/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants