Files
hermes-agent/agent/oneshot.py
Teknium 624a752e79 refactor(agent/runtime): deadline/file_safety/estop/lifecycle leaf modules — dedupe result plumbing, drop dead guard code
- deadline: _result()/_abandon() replace 7 BoundedResult constructions and 2
  cancel+callback sites; timers handled as a list; dead raise_if_timed_out removed.
- file_safety: retired classify_cross_profile_target (0 refs; get_cross_profile_warning
  stub kept for external callers), _home_and_resolved/_mirror_warning shared by
  the sandbox/container mirror guards; _find_sandbox_mirror_segments inlined.
- estop: _hermes_home/_canonical_root now the file_safety helpers;
  _reset_log_state_for_tests inlined into its only test.
- subagent_lifecycle: _validate_request driven by _UNSUPPORTED_REQUEST_FIELDS table.
- turn_liveness: dead start()/_abort_message removed, _emit_warning shared.
- process_bootstrap: _enable_happy_eyeballs reused by the client variant.
- Comment/docstring compaction across the remaining leaf modules.
2026-09-02 13:29:48 -07:00

137 lines
5.1 KiB
Python

"""Shared one-off LLM requests for non-conversational helpers.
A "one-shot" is a single stateless model call outside any conversation: it
never touches session history or prompt caching and returns plain text (commit
messages, rename suggestions, summaries). Call with explicit
``instructions``/``user_input`` or a registered ``template`` + ``variables`` so
prompt engineering stays consistent across CLI/TUI/desktop. Model selection
rides :func:`agent.auxiliary_client.call_llm`: ``main_runtime`` inherits the
live session's provider/model, else ``task`` resolves a cheap backend.
"""
import logging
from typing import Any, Callable, Dict, Optional, Tuple
from agent.auxiliary_client import call_llm, extract_content_or_reasoning
logger = logging.getLogger(__name__)
# Templates are plain callables (not str.format) so diff/code payloads with
# literal "{" / "}" pass through untouched.
PromptTemplate = Callable[[Dict[str, Any]], Tuple[str, str]]
def _truncate(text: str, limit: int) -> str:
text = text or ""
if len(text) <= limit:
return text
return text[:limit].rstrip() + "\n…(truncated)"
_COMMIT_INSTRUCTIONS = (
"You write git commit messages. Given a diff of staged changes, write ONE "
"concise Conventional Commits message describing what the change does and why.\n"
"Rules:\n"
"- Subject line: type(scope): summary — imperative mood, lower-case, no "
"trailing period, ≤ 72 characters. Types: feat, fix, refactor, perf, docs, "
"test, build, chore, style, ci.\n"
"- Omit the scope if it isn't obvious.\n"
"- Add a short body (wrapped at ~72 cols) ONLY when the change needs "
"explanation; skip it for small/obvious changes.\n"
"- Describe the actual change, never restate the diff line-by-line.\n"
"- Return ONLY the commit message text — no quotes, no markdown fences, no "
"preamble."
)
def _commit_message_template(variables: Dict[str, Any]) -> Tuple[str, str]:
diff = _truncate(str(variables.get("diff") or ""), 12000)
recent = _truncate(str(variables.get("recent_commits") or ""), 1500)
parts = []
if recent.strip():
parts.append(
"Recent commit subjects from this repo (match their style/conventions):\n"
f"{recent}"
)
parts.append("Diff to describe:\n" + (diff or "(no textual diff available)"))
# "Regenerate" must yield something new even on greedy/server-pinned
# temperature models; a nonce isn't enough, so hand back the previous
# message and require a genuinely different one.
avoid = _truncate(str(variables.get("avoid") or "").strip(), 1000)
if avoid:
parts.append(
"You already proposed the message below and the user wants a "
"different one. Write a NEW message with different wording (and, if "
"reasonable, a different emphasis or scope framing) — do not repeat "
f"it:\n{avoid}"
)
return _COMMIT_INSTRUCTIONS, "\n\n".join(parts)
# Registry of named templates; add an entry to give a new surface a reusable prompt.
PROMPT_TEMPLATES: Dict[str, PromptTemplate] = {
"commit_message": _commit_message_template,
}
def render_template(name: str, variables: Optional[Dict[str, Any]] = None) -> Tuple[str, str]:
"""Resolve a registered template into (instructions, user_input); KeyError if unknown."""
template = PROMPT_TEMPLATES.get(name)
if template is None:
raise KeyError(f"unknown one-shot template: {name}")
return template(variables or {})
def run_oneshot(
*,
instructions: str = "",
user_input: str = "",
template: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None,
task: str = "title_generation",
max_tokens: int = 1024,
temperature: Optional[float] = 0.3,
timeout: float = 60.0,
main_runtime: Optional[Dict[str, Any]] = None,
) -> str:
"""Run a single stateless LLM request and return its text (fence-stripped).
Raises RuntimeError when no provider is configured (from :func:`call_llm`),
KeyError for an unknown template, ValueError when the prompt is empty.
"""
if template:
instructions, user_input = render_template(template, variables)
if not (instructions or "").strip() and not (user_input or "").strip():
raise ValueError("run_oneshot requires a template or instructions/user_input")
messages = []
if (instructions or "").strip():
messages.append({"role": "system", "content": instructions})
messages.append({"role": "user", "content": user_input or ""})
response = call_llm(
task=task,
messages=messages,
max_tokens=max_tokens,
temperature=temperature,
timeout=timeout,
main_runtime=main_runtime,
)
text = (extract_content_or_reasoning(response) or "").strip()
return _strip_code_fence(text)
def _strip_code_fence(text: str) -> str:
"""Drop a single wrapping ``` fence the model may have added."""
if not text.startswith("```"):
return text
lines = text.splitlines()
if len(lines) >= 2 and lines[0].startswith("```") and lines[-1].strip() == "```":
return "\n".join(lines[1:-1]).strip()
return text