Merge branch 'main' of github.com:NousResearch/hermes-agent into bb/gui
This commit is contained in:
@@ -1503,6 +1503,10 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
|
||||
query=function_args.get("query", ""),
|
||||
role_filter=function_args.get("role_filter"),
|
||||
limit=function_args.get("limit", 3),
|
||||
session_id=function_args.get("session_id"),
|
||||
around_message_id=function_args.get("around_message_id"),
|
||||
window=function_args.get("window", 5),
|
||||
sort=function_args.get("sort"),
|
||||
db=session_db,
|
||||
current_session_id=agent.session_id,
|
||||
)
|
||||
|
||||
+103
-37
@@ -82,6 +82,108 @@ def _ra():
|
||||
return run_agent
|
||||
|
||||
|
||||
def _restore_or_build_system_prompt(agent, system_message, conversation_history):
|
||||
"""Restore the cached system prompt from the session DB or build it fresh.
|
||||
|
||||
Mutates ``agent._cached_system_prompt`` and persists a freshly-built
|
||||
prompt back to the session DB on first build. Extracted from
|
||||
``run_conversation`` so the prefix-cache restore path can be tested in
|
||||
isolation.
|
||||
|
||||
Three-way state distinction for the stored row, surfaced via logs so
|
||||
silent prefix-cache misses are visible in ``agent.log``:
|
||||
|
||||
* ``missing`` — no session row yet (legitimate first turn).
|
||||
* ``null`` — row exists, ``system_prompt`` column is NULL.
|
||||
Legacy session predating system-prompt persistence, or a migration
|
||||
leftover. Warns when ``conversation_history`` is non-empty.
|
||||
* ``empty`` — row exists, ``system_prompt`` column is the empty
|
||||
string. Indicates a previous-turn write that ran but stored
|
||||
nothing (silent persistence bug). Always warns.
|
||||
* ``present`` — row exists with a usable prompt → reused verbatim.
|
||||
|
||||
Read or write failures against the session DB log at WARNING (not
|
||||
DEBUG) so persistent issues (disk full, schema drift, lock contention)
|
||||
surface without needing verbose mode. This used to be a debug-level
|
||||
log that silently broke prefix-cache reuse on the gateway path
|
||||
(which constructs a fresh ``AIAgent`` per turn and depends on this
|
||||
DB roundtrip).
|
||||
"""
|
||||
stored_prompt = None
|
||||
stored_state = "missing"
|
||||
if conversation_history and agent._session_db:
|
||||
try:
|
||||
session_row = agent._session_db.get_session(agent.session_id)
|
||||
if session_row is not None:
|
||||
raw_prompt = session_row.get("system_prompt")
|
||||
if raw_prompt is None:
|
||||
stored_state = "null"
|
||||
elif raw_prompt == "":
|
||||
stored_state = "empty"
|
||||
else:
|
||||
stored_prompt = raw_prompt
|
||||
stored_state = "present"
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Session DB get_session failed for system-prompt restore "
|
||||
"(session=%s): %s. Falling back to fresh build — prefix "
|
||||
"cache will miss for this turn.",
|
||||
agent.session_id, exc,
|
||||
)
|
||||
|
||||
if stored_prompt:
|
||||
# Continuing session — reuse the exact system prompt from the
|
||||
# previous turn so the Anthropic cache prefix matches.
|
||||
agent._cached_system_prompt = stored_prompt
|
||||
return
|
||||
|
||||
if conversation_history and stored_state in ("null", "empty"):
|
||||
# Continuing session whose stored prompt is unusable. The
|
||||
# previous turn's write either never happened or wrote an empty
|
||||
# string — either way every turn now rebuilds and the prefix
|
||||
# cache misses every time.
|
||||
logger.warning(
|
||||
"Stored system prompt for session %s is %s; rebuilding "
|
||||
"from scratch this turn. Prefix cache will miss until "
|
||||
"the rebuild persists. Investigate the previous turn's "
|
||||
"update_system_prompt write path.",
|
||||
agent.session_id, stored_state,
|
||||
)
|
||||
|
||||
# First turn of a new session (or recovering from a broken stored
|
||||
# prompt) — build from scratch.
|
||||
agent._cached_system_prompt = agent._build_system_prompt(system_message)
|
||||
|
||||
# Plugin hook: on_session_start — fired once when a brand-new
|
||||
# session is created (not on continuation). Plugins can use this
|
||||
# to initialise session-scoped state (e.g. warm a memory cache).
|
||||
try:
|
||||
from hermes_cli.plugins import invoke_hook as _invoke_hook
|
||||
_invoke_hook(
|
||||
"on_session_start",
|
||||
session_id=agent.session_id,
|
||||
model=agent.model,
|
||||
platform=getattr(agent, "platform", None) or "",
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("on_session_start hook failed: %s", exc)
|
||||
|
||||
# Persist the system prompt snapshot in SQLite. Failure here used
|
||||
# to log at DEBUG, which silently broke prefix-cache reuse on the
|
||||
# gateway path (fresh AIAgent per turn → reads from this row every
|
||||
# subsequent turn).
|
||||
if agent._session_db:
|
||||
try:
|
||||
agent._session_db.update_system_prompt(agent.session_id, agent._cached_system_prompt)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Session DB update_system_prompt failed for session %s: "
|
||||
"%s. Subsequent turns will rebuild the system prompt and "
|
||||
"miss the prefix cache.",
|
||||
agent.session_id, exc,
|
||||
)
|
||||
|
||||
|
||||
def run_conversation(
|
||||
agent,
|
||||
user_message: str,
|
||||
@@ -313,43 +415,7 @@ def run_conversation(
|
||||
# producing a different system prompt and breaking the Anthropic
|
||||
# prefix cache.
|
||||
if agent._cached_system_prompt is None:
|
||||
stored_prompt = None
|
||||
if conversation_history and agent._session_db:
|
||||
try:
|
||||
session_row = agent._session_db.get_session(agent.session_id)
|
||||
if session_row:
|
||||
stored_prompt = session_row.get("system_prompt") or None
|
||||
except Exception:
|
||||
pass # Fall through to build fresh
|
||||
|
||||
if stored_prompt:
|
||||
# Continuing session — reuse the exact system prompt from
|
||||
# the previous turn so the Anthropic cache prefix matches.
|
||||
agent._cached_system_prompt = stored_prompt
|
||||
else:
|
||||
# First turn of a new session — build from scratch.
|
||||
agent._cached_system_prompt = agent._build_system_prompt(system_message)
|
||||
# Plugin hook: on_session_start
|
||||
# Fired once when a brand-new session is created (not on
|
||||
# continuation). Plugins can use this to initialise
|
||||
# session-scoped state (e.g. warm a memory cache).
|
||||
try:
|
||||
from hermes_cli.plugins import invoke_hook as _invoke_hook
|
||||
_invoke_hook(
|
||||
"on_session_start",
|
||||
session_id=agent.session_id,
|
||||
model=agent.model,
|
||||
platform=getattr(agent, "platform", None) or "",
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("on_session_start hook failed: %s", exc)
|
||||
|
||||
# Store the system prompt snapshot in SQLite
|
||||
if agent._session_db:
|
||||
try:
|
||||
agent._session_db.update_system_prompt(agent.session_id, agent._cached_system_prompt)
|
||||
except Exception as e:
|
||||
logger.debug("Session DB update_system_prompt failed: %s", e)
|
||||
_restore_or_build_system_prompt(agent, system_message, conversation_history)
|
||||
|
||||
active_system_prompt = agent._cached_system_prompt
|
||||
|
||||
|
||||
@@ -274,6 +274,10 @@ TOOL_USE_ENFORCEMENT_MODELS = ("gpt", "codex", "gemini", "gemma", "grok", "glm")
|
||||
# where GPT models abandon work on partial results, skip prerequisite lookups,
|
||||
# hallucinate instead of using tools, and declare "done" without verification.
|
||||
# Inspired by patterns from OpenAI's GPT-5.4 prompting guide & OpenClaw PR #38953.
|
||||
# Also applied to xAI Grok — same failure modes in practice (claims completion
|
||||
# without tool calls, suggests workarounds instead of using existing tools,
|
||||
# replies with plans/suggestions instead of executing). The body is
|
||||
# family-agnostic; the OPENAI_ prefix reflects origin, not exclusivity.
|
||||
OPENAI_MODEL_EXECUTION_GUIDANCE = (
|
||||
"# Execution discipline\n"
|
||||
"<tool_persistence>\n"
|
||||
|
||||
+11
-2
@@ -156,7 +156,10 @@ def build_system_prompt_parts(agent: Any, system_message: Optional[str] = None)
|
||||
stable_parts.append(GOOGLE_MODEL_OPERATIONAL_GUIDANCE)
|
||||
# OpenAI GPT/Codex execution discipline (tool persistence,
|
||||
# prerequisite checks, verification, anti-hallucination).
|
||||
if "gpt" in _model_lower or "codex" in _model_lower:
|
||||
# Also applied to xAI Grok — same failure modes (claims completion
|
||||
# without tool calls, suggests workarounds instead of using
|
||||
# existing tools, replies with plans instead of executing).
|
||||
if "gpt" in _model_lower or "codex" in _model_lower or "grok" in _model_lower:
|
||||
stable_parts.append(OPENAI_MODEL_EXECUTION_GUIDANCE)
|
||||
|
||||
has_skills_tools = any(name in agent.valid_tool_names for name in ['skills_list', 'skill_view', 'skill_manage'])
|
||||
@@ -255,7 +258,13 @@ def build_system_prompt_parts(agent: Any, system_message: Optional[str] = None)
|
||||
|
||||
from hermes_time import now as _hermes_now
|
||||
now = _hermes_now()
|
||||
timestamp_line = f"Conversation started: {now.strftime('%A, %B %d, %Y %I:%M %p')}"
|
||||
# Date-only (not minute-precision) so the system prompt is byte-stable
|
||||
# for the full day. Minute-precision changes invalidate prefix-cache KV
|
||||
# on every rebuild path (compression boundary, fresh-agent gateway turns,
|
||||
# session resume without a stored prompt). The model can still query the
|
||||
# exact wall-clock time via tools when it actually needs it.
|
||||
# Credit: @iamfoz (PR #20451).
|
||||
timestamp_line = f"Conversation started: {now.strftime('%A, %B %d, %Y')}"
|
||||
if agent.pass_session_id and agent.session_id:
|
||||
timestamp_line += f"\nSession ID: {agent.session_id}"
|
||||
if agent.model:
|
||||
|
||||
@@ -622,6 +622,10 @@ def execute_tool_calls_sequential(agent, assistant_message, messages: list, effe
|
||||
query=function_args.get("query", ""),
|
||||
role_filter=function_args.get("role_filter"),
|
||||
limit=function_args.get("limit", 3),
|
||||
session_id=function_args.get("session_id"),
|
||||
around_message_id=function_args.get("around_message_id"),
|
||||
window=function_args.get("window", 5),
|
||||
sort=function_args.get("sort"),
|
||||
db=session_db,
|
||||
current_session_id=agent.session_id,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user