-
Notifications
You must be signed in to change notification settings - Fork 53k
feat(agent): declarative per-fragment system-prompt overrides #44610
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
adambiggs
wants to merge
2
commits into
NousResearch:main
Choose a base branch
from
adambiggs:feat/prompt-fragment-overrides
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| """Declarative, cache-safe overrides for named system-prompt fragments. | ||
|
|
||
| Named fragments assembled into the system prompt | ||
| carry a stable string *key* (see :data:`FRAGMENT_KEYS`). Users reshape any | ||
| of them from ``config.yaml`` under ``agent.prompt_overrides`` without editing | ||
| source: | ||
|
|
||
| .. code-block:: yaml | ||
|
|
||
| agent: | ||
| prompt_overrides: | ||
| task_completion: {mode: replace, text: "..."} | ||
| tool_use_enforcement: {mode: append, text: "..."} | ||
| google_operational: {mode: remove} | ||
| # shorthand: a bare string means replace | ||
| steer_channel: "..." | ||
|
|
||
| Design contract — **overrides are pure data, resolved once at prompt-build | ||
| time**. There is no callable hook and no conditional logic, by design: the | ||
| assembled prompt must stay a deterministic function of (agent, config) so it | ||
| is byte-stable across turns and the upstream prefix cache stays warm. Project | ||
| context and runtime environment facts are outside this named-fragment surface. With no | ||
| overrides configured the output is byte-identical to the un-overridden prompt. | ||
|
|
||
| A fragment override only takes effect when that fragment is actually emitted | ||
| this session (e.g. ``model_identity`` only ships for Alibaba). ``append``/``prepend`` to a fragment that isn't present this session | ||
| silently no-ops — there is nothing to append to. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| from typing import Any, Dict, Optional | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| VALID_MODES = ("replace", "append", "prepend", "remove") | ||
|
|
||
| # Canonical registry of every override-addressable fragment key. Keep in sync | ||
| # with the ``_fragment(...)`` call sites in ``agent/system_prompt.py``. Surfaced to | ||
| # users for discovery (``hermes`` docs / tooling) — an override map keyed by | ||
| # string is only usable if the keys are enumerable. | ||
| FRAGMENT_KEYS: Dict[str, str] = { | ||
| "identity": "Agent identity slot (SOUL.md content, or the default identity).", | ||
| "hermes_help": "Pointer to the hermes-agent skill/docs for questions about Hermes itself.", | ||
| "task_completion": "Universal 'finish the job' / no-fabrication guidance (all models).", | ||
| "parallel_tool_call_guidance": "Universal guidance to batch independent tool calls.", | ||
| "tool_guidance": "Composed per-tool behavioral guidance (memory, session_search, skills, kanban).", | ||
| "steer_channel": "Note explaining the mid-turn steer channel in tool results.", | ||
| "tool_use_enforcement": "Tells the model to actually call tools instead of describing intent.", | ||
| "google_operational": "Gemini/Gemma operational directives (absolute paths, parallel calls, etc.).", | ||
| "execution_discipline": "GPT/Codex/Grok execution discipline (tool persistence, verification).", | ||
| "skills": "Skills system-prompt block (available skills and how to load them).", | ||
| "model_identity": "Explicit model-identity line (Alibaba Coding Plan API name workaround).", | ||
| "environment_probe": "Local Python/pip/uv/PEP-668 toolchain probe line.", | ||
| "active_profile": "Active Hermes profile note and cross-profile write guidance.", | ||
| "platform_hints": "Platform-specific operational hints (from PLATFORM_HINTS or a plugin).", | ||
| "coding_brief": "Coding posture guidance before project context.", | ||
| "coding_workspace": "Pinned workspace snapshot after project context.", | ||
| "coding_instructions": "Configured coding instructions after the workspace snapshot.", | ||
| } | ||
|
|
||
|
|
||
| def normalize_overrides(raw: Any) -> Dict[str, Dict[str, str]]: | ||
| """Validate and normalize ``agent.prompt_overrides`` config into a clean map. | ||
|
|
||
| Accepts the loosely-typed YAML value and returns ``{key: {"mode": ..., | ||
| "text": ...}}`` containing only well-formed entries for known fragment | ||
| keys. Malformed entries are dropped with a warning rather than raising — | ||
| a bad override should never block prompt assembly. | ||
| """ | ||
| if not raw: | ||
| return {} | ||
| if not isinstance(raw, dict): | ||
| logger.warning( | ||
| "agent.prompt_overrides must be a mapping of fragment-key -> override; " | ||
| "got %s. Ignoring.", type(raw).__name__, | ||
| ) | ||
| return {} | ||
|
|
||
| result: Dict[str, Dict[str, str]] = {} | ||
| for key, spec in raw.items(): | ||
| if key not in FRAGMENT_KEYS: | ||
| logger.warning( | ||
| "agent.prompt_overrides: unknown fragment key %r (ignored). " | ||
| "Valid keys: %s", key, ", ".join(sorted(FRAGMENT_KEYS)), | ||
| ) | ||
| continue | ||
|
|
||
| # Shorthand: a bare string is a full replacement. | ||
| if isinstance(spec, str): | ||
| result[key] = {"mode": "replace", "text": spec} | ||
| continue | ||
|
|
||
| if not isinstance(spec, dict): | ||
| logger.warning( | ||
| "agent.prompt_overrides[%r] must be a string or mapping; got %s. " | ||
| "Ignored.", key, type(spec).__name__, | ||
| ) | ||
| continue | ||
|
|
||
| mode = str(spec.get("mode", "replace")).lower().strip() | ||
| if mode not in VALID_MODES: | ||
| logger.warning( | ||
| "agent.prompt_overrides[%r]: invalid mode %r (valid: %s). Ignored.", | ||
| key, mode, ", ".join(VALID_MODES), | ||
| ) | ||
| continue | ||
|
|
||
| if mode == "remove": | ||
| result[key] = {"mode": "remove", "text": ""} | ||
| continue | ||
|
|
||
| text = spec.get("text", "") | ||
| if text is None: | ||
| text = "" | ||
| if not isinstance(text, str): | ||
| logger.warning( | ||
| "agent.prompt_overrides[%r].text must be a string; got %s. Ignored.", | ||
| key, type(text).__name__, | ||
| ) | ||
| continue | ||
| if mode in ("append", "prepend") and not text.strip(): | ||
| logger.warning( | ||
| "agent.prompt_overrides[%r]: %s mode with empty text is a no-op. " | ||
| "Ignored.", key, mode, | ||
| ) | ||
| continue | ||
| result[key] = {"mode": mode, "text": text} | ||
|
|
||
| return result | ||
|
|
||
|
|
||
| def apply_fragment_override( | ||
| overrides: Optional[Dict[str, Dict[str, str]]], | ||
| key: str, | ||
| text: Optional[str], | ||
| ) -> Optional[str]: | ||
| """Apply any configured override for ``key`` to ``text``. | ||
|
|
||
| Returns the (possibly transformed) fragment text, or ``None`` when the | ||
| fragment should be dropped (``remove`` mode, or an empty result). ``text`` | ||
| is the default fragment content Hermes would emit with no override. | ||
| """ | ||
| if not overrides: | ||
| return text | ||
| spec = overrides.get(key) | ||
| if not spec: | ||
| return text | ||
|
|
||
| mode = spec.get("mode", "replace") | ||
| if mode == "remove": | ||
| return None | ||
|
|
||
| new = spec.get("text", "") | ||
| if mode == "replace": | ||
| return new | ||
| if mode == "append": | ||
| if not text or not text.strip(): | ||
| return text | ||
| return f"{text}\n\n{new}" | ||
| if mode == "prepend": | ||
| if not text or not text.strip(): | ||
| return text | ||
| return f"{new}\n\n{text}" | ||
| return text | ||
|
|
||
|
|
||
| __all__ = [ | ||
| "FRAGMENT_KEYS", | ||
| "VALID_MODES", | ||
| "normalize_overrides", | ||
| "apply_fragment_override", | ||
| ] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This registry is documented as canonical, but
agent/system_prompt.py:230in this PR still directly appendsPARALLEL_TOOL_CALL_GUIDANCE. Add a key and route that block throughemit(), or narrow the feature/docs claim so the omission is explicit.